@hajimism/dek 0.6.0 → 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 +20 -0
- package/package.json +1 -1
- package/src/cli/check.ts +20 -11
- package/src/core/lint/script.ts +2 -4
- package/src/core/plugin-host.ts +57 -17
- package/src/core/plugin.ts +15 -9
- package/src/core/script-lines.ts +11 -0
- package/src/core/sync.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
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.1](https://github.com/hajimism/dek/compare/v0.6.0...v0.6.1) (2026-11-09)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* run a project's plugins in dekc check, for the slide it checks ([8f091ca](https://github.com/hajimism/dek/commit/8f091ca1ee65f0c2994f59ad1f40bcb30ac35141))
|
|
11
|
+
* run a project's plugins in dekc check, for the slide it checks ([07cd3cc](https://github.com/hajimism/dek/commit/07cd3ccc14ec5ce0ff299880f2895780438a1510))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* tell the agent to deal with lint's warnings, and which plugin data is whose ([8550412](https://github.com/hajimism/dek/commit/85504129dbe29b3a4b3b92b7033a903ef7c321e2))
|
|
17
|
+
* tell the agent to deal with lint's warnings, and which plugin data is whose ([7e69b3b](https://github.com/hajimism/dek/commit/7e69b3b525421f08de65fe806b525d70504e7923))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Documentation
|
|
21
|
+
|
|
22
|
+
* **adr:** record what AGENTS.md says of warnings and of whose plugin data is ([d42d28f](https://github.com/hajimism/dek/commit/d42d28f4d70572ecdd0e5ea9c15678f69b46af17))
|
|
23
|
+
* say in the lint reference that plugins run in dekc check too ([c7f0cbb](https://github.com/hajimism/dek/commit/c7f0cbbbf49cf3bccab9cc7996f49d8e067d8f73))
|
|
24
|
+
|
|
5
25
|
## [0.6.0](https://github.com/hajimism/dek/compare/v0.5.1...v0.6.0) (2026-11-07)
|
|
6
26
|
|
|
7
27
|
|
package/package.json
CHANGED
package/src/cli/check.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { shellQuote } from "../core/escape.ts";
|
|
|
4
4
|
import type { Fill } from "../core/fill.ts";
|
|
5
5
|
import { lintDeck } from "../core/lint.ts";
|
|
6
6
|
import { type PlaywrightRunner, playwrightMissingError } from "../core/playwright.ts";
|
|
7
|
+
import { checkSlide } from "../core/plugin-host.ts";
|
|
7
8
|
import { runVisualDeck } from "../core/visual.ts";
|
|
8
9
|
import { hasVoice, loadCachedTimeline, VOICE_SETUP_HINT } from "../core/voice.ts";
|
|
9
10
|
import { type SkippedCheck, skippedChecks, visualSkipped } from "./result.ts";
|
|
@@ -35,26 +36,34 @@ export async function checkCommand(
|
|
|
35
36
|
requireSection(deck, slug);
|
|
36
37
|
|
|
37
38
|
const diagnostics = lintDeck({ project, deck }, { slug });
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
// The plugins run in processes of their own while the browser measures the slide.
|
|
40
|
+
const [visual, plugins] = await Promise.all([
|
|
41
|
+
runVisualDeck(
|
|
42
|
+
{ project, deck },
|
|
43
|
+
{
|
|
44
|
+
slug,
|
|
45
|
+
screenshot: options.shot === true,
|
|
46
|
+
...(options.runner ? { runner: options.runner } : {}),
|
|
47
|
+
},
|
|
48
|
+
),
|
|
49
|
+
checkSlide(project, deck, slug),
|
|
50
|
+
]);
|
|
46
51
|
if (visual) {
|
|
47
52
|
diagnostics.push(...visual.diagnostics);
|
|
48
53
|
} else if (options.shot) {
|
|
49
54
|
throw playwrightMissingError();
|
|
50
55
|
}
|
|
56
|
+
diagnostics.push(...plugins.diagnostics);
|
|
51
57
|
|
|
52
58
|
const shot = visual?.screenshotPath;
|
|
53
59
|
const { slug: _slide, ...fill } = visual?.fills.find((found) => found.slug === slug) ?? {};
|
|
54
60
|
|
|
55
|
-
const skipped: SkippedCheck[] =
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
const skipped: SkippedCheck[] = [...plugins.skipped];
|
|
62
|
+
if (!visual) {
|
|
63
|
+
skipped.push(
|
|
64
|
+
visualSkipped(`dekc check ${shellQuote(slug)}`, { reason: "Playwright is not installed" }),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
58
67
|
|
|
59
68
|
// A live-only deck is done without voice, so asking for it skips the check rather than failing,
|
|
60
69
|
// the way a missing Playwright skips visual.
|
package/src/core/lint/script.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Diagnostic, diag } from "../diagnostic.ts";
|
|
2
2
|
import type { DekError } from "../error.ts";
|
|
3
3
|
import { ScriptError } from "../parse.ts";
|
|
4
|
-
import { scriptLines } from "../script-lines.ts";
|
|
4
|
+
import { scriptLines, slugAtLine } from "../script-lines.ts";
|
|
5
5
|
import type { LintContext } from "./context.ts";
|
|
6
6
|
|
|
7
7
|
/** DEK004: a section id or a beat id used twice. */
|
|
@@ -56,7 +56,6 @@ export function strayHeadingDiagnostics(ctx: LintContext): Diagnostic[] {
|
|
|
56
56
|
if (!script) {
|
|
57
57
|
return [];
|
|
58
58
|
}
|
|
59
|
-
const starts = deck.deck.sections.map((section) => ({ line: section.line, slug: section.slug }));
|
|
60
59
|
const diagnostics: Diagnostic[] = [];
|
|
61
60
|
// What the parser reads as text, a code block or a comment, is not a heading here either.
|
|
62
61
|
for (const [offset, { text, literal }] of scriptLines(script.body).entries()) {
|
|
@@ -65,8 +64,7 @@ export function strayHeadingDiagnostics(ctx: LintContext): Diagnostic[] {
|
|
|
65
64
|
continue;
|
|
66
65
|
}
|
|
67
66
|
const line = script.bodyStartLine + offset;
|
|
68
|
-
|
|
69
|
-
const slug = starts.filter((start) => start.line < line).at(-1)?.slug;
|
|
67
|
+
const slug = slugAtLine(deck.deck.sections, line);
|
|
70
68
|
diagnostics.push(
|
|
71
69
|
diag("DEK044", {
|
|
72
70
|
message: `${"#".repeat(level)} heading is not a slide or a beat; it is read as spoken text`,
|
package/src/core/plugin-host.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from "./plugin.ts";
|
|
21
21
|
import type { Project, ProjectDeck } from "./resolve.ts";
|
|
22
22
|
import { readSourceIfExists, writeInside } from "./safe-fs.ts";
|
|
23
|
+
import { slugAtLine } from "./script-lines.ts";
|
|
23
24
|
import { OverrunError, runPiped, workerCommand } from "./spawn.ts";
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -53,14 +54,21 @@ function binName(name: string): string {
|
|
|
53
54
|
* any plugin does, and never from PATH: its code is the dek the project installed, which the
|
|
54
55
|
* project's lockfile pins.
|
|
55
56
|
*/
|
|
56
|
-
const BUNDLED: Record<string, { main: string; page?: string }> = {
|
|
57
|
+
const BUNDLED: Record<string, { main: string; page?: string; guide: string }> = {
|
|
57
58
|
study: bundled("study", { page: true }),
|
|
58
59
|
words: bundled("words", { page: false }),
|
|
59
60
|
};
|
|
60
61
|
|
|
61
|
-
function bundled(
|
|
62
|
+
function bundled(
|
|
63
|
+
name: string,
|
|
64
|
+
{ page }: { page: boolean },
|
|
65
|
+
): { main: string; page?: string; guide: string } {
|
|
62
66
|
const dir = join(import.meta.dir, "..", "plugins", name);
|
|
63
|
-
return {
|
|
67
|
+
return {
|
|
68
|
+
main: join(dir, "main.ts"),
|
|
69
|
+
...(page ? { page: join(dir, "page.ts") } : {}),
|
|
70
|
+
guide: `https://hajimism.github.io/dek/reference/plugins.html#dek-${name}`,
|
|
71
|
+
};
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
/** The page of each plugin dek ships with one, as a file to compile for the browser (ADR 34). */
|
|
@@ -304,7 +312,7 @@ export async function checkDeck(
|
|
|
304
312
|
}
|
|
305
313
|
return { diagnostics: [failed("could not start")] };
|
|
306
314
|
}
|
|
307
|
-
const read = readSarif(stdout, plugin,
|
|
315
|
+
const read = readSarif(stdout, plugin, deck, pluginDir(project.root, plugin.name));
|
|
308
316
|
return typeof read === "string" ? { diagnostics: [failed(read)] } : { diagnostics: read };
|
|
309
317
|
}
|
|
310
318
|
|
|
@@ -340,13 +348,14 @@ type SarifResult = {
|
|
|
340
348
|
};
|
|
341
349
|
|
|
342
350
|
/**
|
|
343
|
-
* The findings in a check's SARIF, or what is wrong with it, as the end of a DEK046 message.
|
|
344
|
-
*
|
|
351
|
+
* The findings in a check's SARIF, or what is wrong with it, as the end of a DEK046 message. A
|
|
352
|
+
* relative location is read against the deck's folder; `ownDir` is the plugin's own.
|
|
345
353
|
*/
|
|
346
354
|
function readSarif(
|
|
347
355
|
stdout: string,
|
|
348
356
|
plugin: ReadyPlugin,
|
|
349
|
-
|
|
357
|
+
deck: ProjectDeck,
|
|
358
|
+
ownDir: string,
|
|
350
359
|
): Diagnostic[] | string {
|
|
351
360
|
let log: unknown;
|
|
352
361
|
try {
|
|
@@ -381,7 +390,7 @@ function readSarif(
|
|
|
381
390
|
id,
|
|
382
391
|
severity: rule.severity,
|
|
383
392
|
message: foreignLine(text, PLUGIN_TEXT_MAX),
|
|
384
|
-
...location(result,
|
|
393
|
+
...location(result, deck, ownDir),
|
|
385
394
|
...(typeof hint === "string" && hint.trim() !== ""
|
|
386
395
|
? { hint: foreignLine(hint, PLUGIN_TEXT_MAX) }
|
|
387
396
|
: {}),
|
|
@@ -394,11 +403,13 @@ function readSarif(
|
|
|
394
403
|
/**
|
|
395
404
|
* Where a finding is, from its first location: a path relative to the deck, or a file URL, inside
|
|
396
405
|
* the deck or the plugin's own `plugins/<name>/` (ADR 35). A location anywhere else is dropped,
|
|
397
|
-
* never shown as a file to change.
|
|
406
|
+
* never shown as a file to change. A finding in a slide's files, or in its section of script.md,
|
|
407
|
+
* is on that slide, as dek's own are.
|
|
398
408
|
*/
|
|
399
409
|
function location(
|
|
400
410
|
result: SarifResult,
|
|
401
|
-
|
|
411
|
+
deck: ProjectDeck,
|
|
412
|
+
ownDir: string,
|
|
402
413
|
): Pick<Diagnostic, "path" | "line" | "column" | "slug"> {
|
|
403
414
|
const physical = result.locations?.[0]?.physicalLocation;
|
|
404
415
|
const uri = physical?.artifactLocation?.uri;
|
|
@@ -407,20 +418,20 @@ function location(
|
|
|
407
418
|
}
|
|
408
419
|
let path: string;
|
|
409
420
|
try {
|
|
410
|
-
path = uri.startsWith("file:") ? fileURLToPath(uri) : resolve(
|
|
421
|
+
path = uri.startsWith("file:") ? fileURLToPath(uri) : resolve(deck.dir, decodeURI(uri));
|
|
411
422
|
} catch {
|
|
412
423
|
return {};
|
|
413
424
|
}
|
|
414
|
-
const inDeck = inside(
|
|
425
|
+
const inDeck = inside(deck.dir, path)?.split(sep).join("/");
|
|
415
426
|
if (inDeck === undefined && inside(ownDir, path) === undefined) {
|
|
416
427
|
return {};
|
|
417
428
|
}
|
|
418
429
|
const line = positive(physical?.region?.startLine);
|
|
419
430
|
const column = line === undefined ? undefined : positive(physical?.region?.startColumn);
|
|
420
|
-
const slug =
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
431
|
+
const slug =
|
|
432
|
+
inDeck === "script.md" && line !== undefined
|
|
433
|
+
? slugAtLine(deck.deck.sections, line)
|
|
434
|
+
: inDeck?.match(/^slides\/([^/]+)\.(?:html|css|ts)$/)?.[1];
|
|
424
435
|
return {
|
|
425
436
|
path,
|
|
426
437
|
...(line === undefined ? {} : { line }),
|
|
@@ -530,6 +541,30 @@ export async function pluginChecks(
|
|
|
530
541
|
return { ready, skipped, diagnostics };
|
|
531
542
|
}
|
|
532
543
|
|
|
544
|
+
/**
|
|
545
|
+
* The plugins' findings for `dekc check` on one slide, and the plugins that did not run. Each
|
|
546
|
+
* pinned plugin checks the deck, as lint has it, and the slide keeps what is on it. It also keeps
|
|
547
|
+
* an error on no slide, such as data that does not read or a plugin that failed: the slide's
|
|
548
|
+
* check may rest on it, and leaving it out would pass a slide no plugin checked.
|
|
549
|
+
*/
|
|
550
|
+
export async function checkSlide(
|
|
551
|
+
project: Project,
|
|
552
|
+
deck: ProjectDeck,
|
|
553
|
+
slug: string,
|
|
554
|
+
): Promise<{ diagnostics: Diagnostic[]; skipped: SkippedCheck[] }> {
|
|
555
|
+
const plugins = await pluginChecks(project);
|
|
556
|
+
const outcomes = await Promise.all(
|
|
557
|
+
plugins.ready.map((plugin) => checkDeck(plugin, project, deck)),
|
|
558
|
+
);
|
|
559
|
+
const diagnostics = [
|
|
560
|
+
...plugins.diagnostics,
|
|
561
|
+
...outcomes.flatMap((outcome) => outcome.diagnostics),
|
|
562
|
+
].filter((found) =>
|
|
563
|
+
found.slug === undefined ? found.severity === "error" : found.slug === slug,
|
|
564
|
+
);
|
|
565
|
+
return { diagnostics, skipped: plugins.skipped };
|
|
566
|
+
}
|
|
567
|
+
|
|
533
568
|
/** What `dekc sync` did with one plugin, for its `--json` and its notes. */
|
|
534
569
|
export type PluginSync = {
|
|
535
570
|
name: string;
|
|
@@ -628,7 +663,12 @@ export function pinnedPlugins(root: string): PinnedPlugin[] {
|
|
|
628
663
|
} catch {
|
|
629
664
|
reading = undefined;
|
|
630
665
|
}
|
|
631
|
-
|
|
666
|
+
const guide = BUNDLED[name]?.guide;
|
|
667
|
+
return {
|
|
668
|
+
name,
|
|
669
|
+
manifest: reading?.ok ? reading.manifest : undefined,
|
|
670
|
+
...(guide === undefined ? {} : { guide }),
|
|
671
|
+
};
|
|
632
672
|
});
|
|
633
673
|
}
|
|
634
674
|
|
package/src/core/plugin.ts
CHANGED
|
@@ -359,30 +359,35 @@ function checkChanges(
|
|
|
359
359
|
return changes;
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
/**
|
|
363
|
-
|
|
362
|
+
/**
|
|
363
|
+
* A plugin dek.toml names, with the manifest its lock pins when it has one, and dek's own page on
|
|
364
|
+
* how to write its data when dek ships it.
|
|
365
|
+
*/
|
|
366
|
+
export type PinnedPlugin = { name: string; manifest: PluginManifest | undefined; guide?: string };
|
|
364
367
|
|
|
365
368
|
/**
|
|
366
369
|
* The plugins in AGENTS.md: one line each, made from the lock and never from the executable, so
|
|
367
370
|
* every sync writes the same block, the dev server's included. Each line is dek's words about
|
|
368
|
-
* checked fields; the plugin's own summaries stay out.
|
|
371
|
+
* checked fields; the plugin's own summaries stay out. A data file says whether it is the
|
|
372
|
+
* project's or each deck's, since the two can share a name. Empty when dek.toml names no plugin.
|
|
369
373
|
*/
|
|
370
374
|
export function pluginsSection(plugins: PinnedPlugin[]): string {
|
|
371
375
|
if (plugins.length === 0) {
|
|
372
376
|
return "";
|
|
373
377
|
}
|
|
374
|
-
const lines = plugins.map(({ name, manifest }) => {
|
|
378
|
+
const lines = plugins.map(({ name, manifest, guide }) => {
|
|
375
379
|
if (!manifest) {
|
|
376
380
|
return `- \`${name}\`: not pinned yet; \`dekc sync\` pins it.`;
|
|
377
381
|
}
|
|
378
382
|
const facts = [
|
|
379
|
-
...(manifest.check
|
|
380
|
-
? [`\`dekc lint\` checks ${ruleList(Object.keys(manifest.check.rules))}.`]
|
|
381
|
-
: []),
|
|
383
|
+
...(manifest.check ? [`It checks ${ruleList(Object.keys(manifest.check.rules))}.`] : []),
|
|
382
384
|
...(manifest.data && manifest.data.length > 0
|
|
383
385
|
? [
|
|
384
386
|
`Its data: ${projectFirst(manifest.data)
|
|
385
|
-
.map(
|
|
387
|
+
.map(
|
|
388
|
+
(entry) =>
|
|
389
|
+
`\`${dataPath(name, entry)}\` ${entry.in === "project" ? "in the project" : "in each deck"}`,
|
|
390
|
+
)
|
|
386
391
|
.join(", ")}.`,
|
|
387
392
|
]
|
|
388
393
|
: []),
|
|
@@ -391,13 +396,14 @@ export function pluginsSection(plugins: PinnedPlugin[]): string {
|
|
|
391
396
|
`Its page takes the ${manifest.page.keys.length === 1 ? "key" : "keys"} ${manifest.page.keys.map((key) => `\`${key}\``).join(", ")}.`,
|
|
392
397
|
]
|
|
393
398
|
: []),
|
|
399
|
+
...(guide && manifest.data && manifest.data.length > 0 ? [`How to write it: ${guide}`] : []),
|
|
394
400
|
];
|
|
395
401
|
return `- \`${name}\` ${manifest.version}${facts.length > 0 ? `: ${facts.join(" ")}` : ""}`;
|
|
396
402
|
});
|
|
397
403
|
return `
|
|
398
404
|
## Plugins
|
|
399
405
|
|
|
400
|
-
Checks installed beside dek, named in \`dek.toml\` \`[plugins]\` and pinned in \`plugins/<name>/plugin.json\`. \`dekc lint\` runs them with dek's own rules; their findings carry the plugin's rule ids.
|
|
406
|
+
Checks installed beside dek, named in \`dek.toml\` \`[plugins]\` and pinned in \`plugins/<name>/plugin.json\`. \`dekc lint\` runs them with dek's own rules, and \`dekc check <slug>\` reports what they find on that slide; their findings carry the plugin's rule ids.
|
|
401
407
|
|
|
402
408
|
${lines.join("\n")}
|
|
403
409
|
`;
|
package/src/core/script-lines.ts
CHANGED
|
@@ -51,3 +51,14 @@ function commentOpenAfter(text: string, open: boolean): boolean {
|
|
|
51
51
|
}
|
|
52
52
|
return inside;
|
|
53
53
|
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The slide a line of script.md belongs to: the `##` section it is in, heading included. Above the
|
|
57
|
+
* first heading it belongs to the deck, and to no slide.
|
|
58
|
+
*/
|
|
59
|
+
export function slugAtLine(
|
|
60
|
+
sections: ReadonlyArray<{ line: number; slug: string }>,
|
|
61
|
+
line: number,
|
|
62
|
+
): string | undefined {
|
|
63
|
+
return sections.filter((section) => section.line <= line).at(-1)?.slug;
|
|
64
|
+
}
|
package/src/core/sync.ts
CHANGED
|
@@ -288,7 +288,7 @@ A build system for talks. Write what you will say; dek builds, measures, and shi
|
|
|
288
288
|
|
|
289
289
|
## Before you report a deck as done
|
|
290
290
|
|
|
291
|
-
- \`dekc lint --visual\` passes.
|
|
291
|
+
- \`dekc lint --visual\` passes. Warnings do not fail it: fix each one, or tell the author which you kept and why.
|
|
292
292
|
- You have read \`dekc shot --sheet\` and judged the deck's balance.
|
|
293
293
|
- Say what you could not judge, such as the argument and the timing, and leave it to the author.
|
|
294
294
|
${referencesSection(root)}${pluginsSection(pinnedPlugins(root))}
|