@kadal/deck 0.0.0-stage → 0.1.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/LICENSE +661 -0
- package/LICENSING.md +40 -0
- package/README.md +170 -2
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/audience/index.d.ts +14 -0
- package/dist/audience/index.js +680 -0
- package/dist/backgrounds/index.d.ts +65 -0
- package/dist/backgrounds/index.js +604 -0
- package/dist/backgrounds/shared.d.ts +7 -0
- package/dist/backgrounds/shared.js +89 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +8 -0
- package/dist/cli/host.js +111 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.js +140 -0
- package/dist/cli/login.js +56 -0
- package/dist/cli/publish.js +159 -0
- package/dist/cli/vite.js +39 -0
- package/dist/compiler/code-meta.d.ts +33 -0
- package/dist/compiler/code-meta.js +81 -0
- package/dist/compiler/index.d.ts +8 -0
- package/dist/compiler/index.js +8 -0
- package/dist/compiler/magic-move.d.ts +26 -0
- package/dist/compiler/magic-move.js +33 -0
- package/dist/compiler/plain-text.js +7 -0
- package/dist/compiler/rehype-code.d.ts +18 -0
- package/dist/compiler/rehype-code.js +66 -0
- package/dist/compiler/rehype-glossary.d.ts +35 -0
- package/dist/compiler/rehype-glossary.js +211 -0
- package/dist/compiler/slots.d.ts +3 -0
- package/dist/compiler/slots.js +36 -0
- package/dist/compiler/split.d.ts +63 -0
- package/dist/compiler/split.js +195 -0
- package/dist/components/Background.d.ts +15 -0
- package/dist/components/Background.js +24 -0
- package/dist/components/Click.d.ts +54 -0
- package/dist/components/Click.js +129 -0
- package/dist/components/Code.d.ts +27 -0
- package/dist/components/Code.js +124 -0
- package/dist/components/Motion.d.ts +25 -0
- package/dist/components/Motion.js +45 -0
- package/dist/components/Sketch.d.ts +44 -0
- package/dist/components/Sketch.js +141 -0
- package/dist/components/Term.d.ts +63 -0
- package/dist/components/Term.js +492 -0
- package/dist/components/bling.d.ts +75 -0
- package/dist/components/bling.js +146 -0
- package/dist/components/content.d.ts +232 -0
- package/dist/components/content.js +637 -0
- package/dist/components/index.d.ts +10 -0
- package/dist/components/index.js +88 -0
- package/dist/core/clicks.d.ts +45 -0
- package/dist/core/clicks.js +75 -0
- package/dist/core/hash.d.ts +33 -0
- package/dist/core/hash.js +49 -0
- package/dist/core/keyboard.d.ts +17 -0
- package/dist/core/keyboard.js +198 -0
- package/dist/core/layout.d.ts +16 -0
- package/dist/core/layout.js +25 -0
- package/dist/core/model.d.ts +259 -0
- package/dist/core/model.js +377 -0
- package/dist/core/navigation.d.ts +74 -0
- package/dist/core/navigation.js +244 -0
- package/dist/core/poster.js +30 -0
- package/dist/core/store.d.ts +20 -0
- package/dist/core/store.js +64 -0
- package/dist/core/touch.js +27 -0
- package/dist/draw/DrawingLayer.js +315 -0
- package/dist/draw/strokes.js +188 -0
- package/dist/export/index.d.ts +68 -0
- package/dist/export/index.js +251 -0
- package/dist/export/run.js +81 -0
- package/dist/glossary/model.d.ts +81 -0
- package/dist/glossary/model.js +156 -0
- package/dist/glossary/parse.d.ts +27 -0
- package/dist/glossary/parse.js +90 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +30 -0
- package/dist/live/LiveProvider.d.ts +20 -0
- package/dist/live/LiveProvider.js +450 -0
- package/dist/live/analytics.d.ts +15 -0
- package/dist/live/analytics.js +103 -0
- package/dist/live/client.d.ts +51 -0
- package/dist/live/client.js +193 -0
- package/dist/live/components.d.ts +64 -0
- package/dist/live/components.js +471 -0
- package/dist/live/context.d.ts +15 -0
- package/dist/live/context.js +28 -0
- package/dist/live/index.d.ts +7 -0
- package/dist/live/index.js +7 -0
- package/dist/live/protocol.d.ts +284 -0
- package/dist/live/protocol.js +42 -0
- package/dist/live/turnstile.js +49 -0
- package/dist/presenter/index.d.ts +20 -0
- package/dist/presenter/index.js +1211 -0
- package/dist/react/AutoAnimate.js +92 -0
- package/dist/react/Backgrounds.d.ts +11 -0
- package/dist/react/Backgrounds.js +154 -0
- package/dist/react/ConfettiLayer.js +85 -0
- package/dist/react/Deck.d.ts +35 -0
- package/dist/react/Deck.js +983 -0
- package/dist/react/Frame.d.ts +16 -0
- package/dist/react/Frame.js +174 -0
- package/dist/react/Hints.d.ts +3 -0
- package/dist/react/Hints.js +201 -0
- package/dist/react/Lightbox.js +54 -0
- package/dist/react/Overview.js +93 -0
- package/dist/react/PrintView.js +128 -0
- package/dist/react/ScrollView.js +128 -0
- package/dist/react/SlideFrame.d.ts +51 -0
- package/dist/react/SlideFrame.js +323 -0
- package/dist/react/Stage.js +151 -0
- package/dist/react/ThemeMenu.d.ts +5 -0
- package/dist/react/ThemeMenu.js +124 -0
- package/dist/react/Zoom.js +23 -0
- package/dist/react/chrome.js +339 -0
- package/dist/react/context.d.ts +165 -0
- package/dist/react/context.js +19 -0
- package/dist/react/hooks.d.ts +33 -0
- package/dist/react/hooks.js +70 -0
- package/dist/react/layouts.d.ts +21 -0
- package/dist/react/layouts.js +104 -0
- package/dist/react/mdx.d.ts +4 -0
- package/dist/react/mdx.js +33 -0
- package/dist/react/terms.d.ts +6 -0
- package/dist/react/terms.js +66 -0
- package/dist/renderers/Island.d.ts +57 -0
- package/dist/renderers/Island.js +223 -0
- package/dist/renderers/contract.d.ts +173 -0
- package/dist/renderers/contract.js +152 -0
- package/dist/renderers/index.d.ts +12 -0
- package/dist/renderers/index.js +16 -0
- package/dist/renderers/react.d.ts +16 -0
- package/dist/renderers/react.js +32 -0
- package/dist/renderers/web-components.d.ts +4 -0
- package/dist/renderers/web-components.js +103 -0
- package/dist/sketch/Canvas.js +49 -0
- package/dist/sketch/dsl.d.ts +131 -0
- package/dist/sketch/dsl.js +281 -0
- package/dist/sketch/index.d.ts +4 -0
- package/dist/sketch/index.js +3 -0
- package/dist/sketch/palette.d.ts +21 -0
- package/dist/sketch/palette.js +66 -0
- package/dist/sketch/render.d.ts +43 -0
- package/dist/sketch/render.js +198 -0
- package/dist/themes/base.css +1704 -0
- package/dist/themes/bling.css +592 -0
- package/dist/themes/glossary.css +317 -0
- package/dist/themes/live.css +1557 -0
- package/dist/themes/minimal.css +83 -0
- package/dist/themes/rla.css +92 -0
- package/dist/themes/sketch.css +121 -0
- package/dist/themes/stack.css +62 -0
- package/dist/vite/index.d.ts +15 -0
- package/dist/vite/index.js +320 -0
- package/package.json +159 -4
- package/tools/verify-island.mjs +150 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
//#region src/backgrounds/shared.ts
|
|
3
|
+
/**
|
|
4
|
+
* A canvas that fills its layer, redraws at the device's pixel ratio, and animates only while
|
|
5
|
+
* the slide is showing — or never, when the viewer asked for reduced motion (one still frame).
|
|
6
|
+
*/
|
|
7
|
+
function useCanvas(active, draw, deps) {
|
|
8
|
+
const ref = useRef(null);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const c = ref.current;
|
|
11
|
+
if (!c) return;
|
|
12
|
+
const ctx = c.getContext("2d");
|
|
13
|
+
if (!ctx) return;
|
|
14
|
+
let raf = 0;
|
|
15
|
+
let last = performance.now();
|
|
16
|
+
let w = 0;
|
|
17
|
+
let h = 0;
|
|
18
|
+
const still = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
|
|
19
|
+
const animating = active !== false && !still;
|
|
20
|
+
/**
|
|
21
|
+
* Draw in the canvas's own units, but only buy the pixels it is actually shown at.
|
|
22
|
+
*
|
|
23
|
+
* A background inside a mirror or a preview is laid out at the deck's full stage size — 1920 ×
|
|
24
|
+
* 1080 — while being displayed, after its ancestors' transform, at a few hundred pixels. Sized
|
|
25
|
+
* off the layout box alone, every one of those asked for a 3840 × 2160 bitmap: 31 MB each,
|
|
26
|
+
* two of them mounted at once the moment the join page is expanded, on top of whatever the
|
|
27
|
+
* slide's embedded pages are holding. A phone does not have that to give — iOS caps what a
|
|
28
|
+
* page may keep in canvas backing store and then clamps or drops the allocation, which is how
|
|
29
|
+
* a background ends up stretched out of the deck's ratio, or the tab ends up gone.
|
|
30
|
+
*
|
|
31
|
+
* So the drawing stays in layout units (every background looks the same everywhere) and the
|
|
32
|
+
* bitmap follows what is on screen: `shown` is the ancestors' scale, read from the rendered
|
|
33
|
+
* box. A full-size stage has shown ≈ 1 and is unchanged.
|
|
34
|
+
*/
|
|
35
|
+
const size = () => {
|
|
36
|
+
const dpr = Math.min(2, window.devicePixelRatio || 1);
|
|
37
|
+
w = c.clientWidth;
|
|
38
|
+
h = c.clientHeight;
|
|
39
|
+
const rect = c.getBoundingClientRect();
|
|
40
|
+
const shown = w > 0 && rect.width > 0 ? rect.width / w : 1;
|
|
41
|
+
const scale = Math.min(dpr, Math.max(.35, shown * dpr));
|
|
42
|
+
c.width = Math.max(1, Math.floor(w * scale));
|
|
43
|
+
c.height = Math.max(1, Math.floor(h * scale));
|
|
44
|
+
ctx.setTransform(scale, 0, 0, scale, 0, 0);
|
|
45
|
+
if (!animating) draw(ctx, w, h, 0, 0);
|
|
46
|
+
};
|
|
47
|
+
size();
|
|
48
|
+
const ro = new ResizeObserver(size);
|
|
49
|
+
ro.observe(c);
|
|
50
|
+
window.addEventListener("resize", size);
|
|
51
|
+
window.addEventListener("orientationchange", size);
|
|
52
|
+
const tick = (t) => {
|
|
53
|
+
const dt = Math.min(.05, (t - last) / 1e3);
|
|
54
|
+
last = t;
|
|
55
|
+
draw(ctx, w, h, t / 1e3, dt);
|
|
56
|
+
raf = requestAnimationFrame(tick);
|
|
57
|
+
};
|
|
58
|
+
if (animating) raf = requestAnimationFrame(tick);
|
|
59
|
+
return () => {
|
|
60
|
+
cancelAnimationFrame(raf);
|
|
61
|
+
ro.disconnect();
|
|
62
|
+
window.removeEventListener("resize", size);
|
|
63
|
+
window.removeEventListener("orientationchange", size);
|
|
64
|
+
};
|
|
65
|
+
}, [active, ...deps]);
|
|
66
|
+
return ref;
|
|
67
|
+
}
|
|
68
|
+
const asList = (v, fallback) => Array.isArray(v) ? v.map(String) : typeof v === "string" ? v.split(",").map((s) => s.trim()) : fallback;
|
|
69
|
+
const asNum = (v, fallback) => typeof v === "number" ? v : typeof v === "string" && v.trim() ? Number(v) : fallback;
|
|
70
|
+
/** A small value-noise, enough for waves and lines. */
|
|
71
|
+
function noise2(x, y) {
|
|
72
|
+
const s = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453;
|
|
73
|
+
return s - Math.floor(s);
|
|
74
|
+
}
|
|
75
|
+
function smoothNoise(x, y) {
|
|
76
|
+
const ix = Math.floor(x);
|
|
77
|
+
const iy = Math.floor(y);
|
|
78
|
+
const fx = x - ix;
|
|
79
|
+
const fy = y - iy;
|
|
80
|
+
const a = noise2(ix, iy);
|
|
81
|
+
const b = noise2(ix + 1, iy);
|
|
82
|
+
const c = noise2(ix, iy + 1);
|
|
83
|
+
const d = noise2(ix + 1, iy + 1);
|
|
84
|
+
const ux = fx * fx * (3 - 2 * fx);
|
|
85
|
+
const uy = fy * fy * (3 - 2 * fy);
|
|
86
|
+
return a + (b - a) * ux + (c - a) * uy + (a - b - c + d) * ux * uy;
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
export { asList, asNum, noise2, smoothNoise, useCanvas };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
package/dist/cli/bin.js
ADDED
package/dist/cli/host.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
//#region src/cli/host.ts
|
|
5
|
+
/**
|
|
6
|
+
* Talking to a Kadal Deck host (https://deck.kadal.cc, or a self-hosted one): where the
|
|
7
|
+
* credentials live, and one `request` that turns the host's `{error, message}` answers into
|
|
8
|
+
* errors a person can act on.
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_HOST = "https://deck.kadal.cc";
|
|
11
|
+
function credentialsPath(env = process.env) {
|
|
12
|
+
const base = env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
13
|
+
return join(base, "kadal-deck", "credentials.json");
|
|
14
|
+
}
|
|
15
|
+
/** KADAL_DECK_TOKEN / KADAL_DECK_HOST win over the saved file (for CI). */
|
|
16
|
+
async function loadCredentials(env = process.env) {
|
|
17
|
+
let saved = {};
|
|
18
|
+
try {
|
|
19
|
+
saved = JSON.parse(await readFile(credentialsPath(env), "utf8"));
|
|
20
|
+
} catch {}
|
|
21
|
+
const token = env.KADAL_DECK_TOKEN || saved.token;
|
|
22
|
+
if (!token) return null;
|
|
23
|
+
return {
|
|
24
|
+
host: normaliseHost(env.KADAL_DECK_HOST || saved.host || "https://deck.kadal.cc"),
|
|
25
|
+
token,
|
|
26
|
+
workspace: env.KADAL_DECK_TOKEN ? void 0 : saved.workspace
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async function saveCredentials(creds, env = process.env) {
|
|
30
|
+
const path = credentialsPath(env);
|
|
31
|
+
await mkdir(dirname(path), {
|
|
32
|
+
recursive: true,
|
|
33
|
+
mode: 448
|
|
34
|
+
});
|
|
35
|
+
await writeFile(path, `${JSON.stringify(creds, null, 2)}\n`, { mode: 384 });
|
|
36
|
+
await chmod(path, 384);
|
|
37
|
+
return path;
|
|
38
|
+
}
|
|
39
|
+
async function clearCredentials(env = process.env) {
|
|
40
|
+
try {
|
|
41
|
+
await rm(credentialsPath(env));
|
|
42
|
+
return true;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function hostFrom(env = process.env, flag) {
|
|
48
|
+
return normaliseHost(flag || env.KADAL_DECK_HOST || "https://deck.kadal.cc");
|
|
49
|
+
}
|
|
50
|
+
function normaliseHost(host) {
|
|
51
|
+
return host.replace(/\/+$/, "");
|
|
52
|
+
}
|
|
53
|
+
/** An answer from the host that was not a success, with what the person should do about it. */
|
|
54
|
+
var HostError = class extends Error {
|
|
55
|
+
status;
|
|
56
|
+
code;
|
|
57
|
+
body;
|
|
58
|
+
constructor(status, code, message, body = null) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.status = status;
|
|
61
|
+
this.code = code;
|
|
62
|
+
this.body = body;
|
|
63
|
+
this.name = "HostError";
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
/** `host` + `/api/v1` + `path`, JSON in and out. Throws `HostError` with a helpful message. */
|
|
67
|
+
async function request(host, path, opts = {}) {
|
|
68
|
+
const doFetch = opts.fetch ?? fetch;
|
|
69
|
+
const headers = { accept: "application/json" };
|
|
70
|
+
if (opts.token) headers.authorization = `Bearer ${opts.token}`;
|
|
71
|
+
let body;
|
|
72
|
+
if (opts.json !== void 0) {
|
|
73
|
+
headers["content-type"] = "application/json";
|
|
74
|
+
body = JSON.stringify(opts.json);
|
|
75
|
+
} else if (opts.body) {
|
|
76
|
+
headers["content-type"] = opts.contentType ?? "application/octet-stream";
|
|
77
|
+
body = opts.body;
|
|
78
|
+
}
|
|
79
|
+
const res = await doFetch(`${host}/api/v1${path}`, {
|
|
80
|
+
method: opts.method ?? (body ? "POST" : "GET"),
|
|
81
|
+
headers,
|
|
82
|
+
body
|
|
83
|
+
});
|
|
84
|
+
const text = await res.text();
|
|
85
|
+
let data = null;
|
|
86
|
+
try {
|
|
87
|
+
data = text ? JSON.parse(text) : null;
|
|
88
|
+
} catch {
|
|
89
|
+
data = text;
|
|
90
|
+
}
|
|
91
|
+
if (res.ok || opts.accept?.includes(res.status)) return {
|
|
92
|
+
status: res.status,
|
|
93
|
+
data
|
|
94
|
+
};
|
|
95
|
+
const err = data && typeof data === "object" ? data : {};
|
|
96
|
+
throw new HostError(res.status, err.error ?? `http_${res.status}`, explain(res.status, host, err.message), data);
|
|
97
|
+
}
|
|
98
|
+
function explain(status, host, message) {
|
|
99
|
+
const said = message ? `${message}` : "";
|
|
100
|
+
switch (status) {
|
|
101
|
+
case 401: return `Not signed in, or the token was revoked. Run \`kadal-deck login\` (or set KADAL_DECK_TOKEN).${said ? ` (${said})` : ""}`;
|
|
102
|
+
case 402: return `${said || "This would go over your plan's limits."} Upgrade at ${host}/app/billing.`;
|
|
103
|
+
case 403: return said || "That isn't allowed with this token.";
|
|
104
|
+
case 404: return said || "Not found: check the workspace handle (--workspace) and that the deck exists.";
|
|
105
|
+
case 413: return said || "A file is too large for the host (25 MB per file).";
|
|
106
|
+
case 429: return said || "Too many requests; wait a minute and try again.";
|
|
107
|
+
default: return said || `The host answered ${status}.`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
export { DEFAULT_HOST, HostError, clearCredentials, credentialsPath, hostFrom, loadCredentials, normaliseHost, request, saveCredentials };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
//#region src/cli/index.d.ts
|
|
2
|
+
export declare const HELP = "kadal-deck <command>\n\n dev run the deck locally with live reload\n build build the deck into dist/ (any static host can serve it)\n export PDF (and --png, --pptx) into export/\n login sign in to a Kadal Deck host (opens the browser)\n logout forget the saved token on this machine\n whoami show the signed-in account and workspace\n publish build and publish the deck; prints its URL\n --slug <slug> --workspace <handle> --title <title> --host <url>\n\nDocs: https://deck.kadal.cc/docs/";
|
|
3
|
+
export declare function main(argv: string[], cwd?: string): Promise<number>;
|
|
4
|
+
//#endregion
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { clearCredentials, hostFrom, loadCredentials, request, saveCredentials } from "./host.js";
|
|
2
|
+
import { login } from "./login.js";
|
|
3
|
+
import { formatBytes, publish, slugFrom, titleFrom } from "./publish.js";
|
|
4
|
+
import { runVite } from "./vite.js";
|
|
5
|
+
import { basename, join, resolve } from "node:path";
|
|
6
|
+
import { readFile, rm } from "node:fs/promises";
|
|
7
|
+
//#region src/cli/index.ts
|
|
8
|
+
/**
|
|
9
|
+
* kadal-deck — the command line for a Kadal Deck folder (bin: ./bin.ts).
|
|
10
|
+
*
|
|
11
|
+
* kadal-deck dev [vite flags] the deck with live reload (vite)
|
|
12
|
+
* kadal-deck build [vite flags] dist/ for any static host (vite build)
|
|
13
|
+
* kadal-deck export [--png] [--pptx] PDF/PNG/PPTX into export/ (needs playwright, pptxgenjs)
|
|
14
|
+
* kadal-deck login [--host URL] sign this machine in to a Kadal Deck host
|
|
15
|
+
* kadal-deck logout forget the saved token
|
|
16
|
+
* kadal-deck whoami who and which workspace the token is for
|
|
17
|
+
* kadal-deck publish [--slug s] [--workspace handle] [--title t]
|
|
18
|
+
*
|
|
19
|
+
* KADAL_DECK_TOKEN and KADAL_DECK_HOST (default https://deck.kadal.cc) override the saved
|
|
20
|
+
* credentials, which is how CI publishes.
|
|
21
|
+
*/
|
|
22
|
+
const HELP = `kadal-deck <command>
|
|
23
|
+
|
|
24
|
+
dev run the deck locally with live reload
|
|
25
|
+
build build the deck into dist/ (any static host can serve it)
|
|
26
|
+
export PDF (and --png, --pptx) into export/
|
|
27
|
+
login sign in to a Kadal Deck host (opens the browser)
|
|
28
|
+
logout forget the saved token on this machine
|
|
29
|
+
whoami show the signed-in account and workspace
|
|
30
|
+
publish build and publish the deck; prints its URL
|
|
31
|
+
--slug <slug> --workspace <handle> --title <title> --host <url>
|
|
32
|
+
|
|
33
|
+
Docs: https://deck.kadal.cc/docs/`;
|
|
34
|
+
async function main(argv, cwd = process.cwd()) {
|
|
35
|
+
const [command, ...rest] = argv;
|
|
36
|
+
const flag = (name) => {
|
|
37
|
+
const i = rest.indexOf(name);
|
|
38
|
+
return i >= 0 ? rest[i + 1] : void 0;
|
|
39
|
+
};
|
|
40
|
+
const out = (line) => console.log(line);
|
|
41
|
+
switch (command) {
|
|
42
|
+
case "dev":
|
|
43
|
+
await runVite(cwd, rest);
|
|
44
|
+
return 0;
|
|
45
|
+
case "build":
|
|
46
|
+
await runVite(cwd, ["build", ...rest]);
|
|
47
|
+
return 0;
|
|
48
|
+
case "export": {
|
|
49
|
+
const { runExport } = await import("../export/run.js");
|
|
50
|
+
await runExport(rest, cwd);
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
case "login": {
|
|
54
|
+
const creds = await login({
|
|
55
|
+
host: hostFrom(process.env, flag("--host")),
|
|
56
|
+
log: out
|
|
57
|
+
});
|
|
58
|
+
const path = await saveCredentials(creds);
|
|
59
|
+
out(`Signed in${creds.workspace ? ` to @${creds.workspace}` : ""}. Token saved to ${path}`);
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
case "logout":
|
|
63
|
+
out(await clearCredentials() ? "Signed out on this machine. Revoke the token in the dashboard (Tokens) if it may have leaked." : "Not signed in.");
|
|
64
|
+
return 0;
|
|
65
|
+
case "whoami": {
|
|
66
|
+
const creds = await loadCredentials();
|
|
67
|
+
if (!creds) {
|
|
68
|
+
out("Not signed in. Run `kadal-deck login`.");
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
const { data } = await request(creds.host, "/me", { token: creds.token });
|
|
72
|
+
out(`${data.user.name ?? ""} <${data.user.email ?? "?"}> on ${creds.host}`.trim());
|
|
73
|
+
if (data.current) out(`workspace @${data.current.handle}${data.current.plan ? ` · ${data.current.plan} plan` : ""}`);
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
case "publish": return publishCommand(cwd, flag, out);
|
|
77
|
+
case void 0:
|
|
78
|
+
case "help":
|
|
79
|
+
case "--help":
|
|
80
|
+
case "-h":
|
|
81
|
+
out(HELP);
|
|
82
|
+
return 0;
|
|
83
|
+
case "--version":
|
|
84
|
+
case "-v": {
|
|
85
|
+
const url = new URL("../../package.json", import.meta.url);
|
|
86
|
+
out(JSON.parse(await readFile(url, "utf8")).version);
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
default:
|
|
90
|
+
out(`Unknown command: ${command}\n\n${HELP}`);
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function publishCommand(cwd, flag, out) {
|
|
95
|
+
const creds = await loadCredentials();
|
|
96
|
+
if (!creds) {
|
|
97
|
+
out("Not signed in. Run `kadal-deck login`, or set KADAL_DECK_TOKEN.");
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
const host = flag("--host") ? hostFrom(process.env, flag("--host")) : creds.host;
|
|
101
|
+
let workspace = flag("--workspace") ?? creds.workspace;
|
|
102
|
+
if (!workspace) {
|
|
103
|
+
const { data } = await request(host, "/me", { token: creds.token });
|
|
104
|
+
workspace = data.current?.handle;
|
|
105
|
+
if (!workspace) throw new Error("This account has no workspace yet; open the dashboard once.");
|
|
106
|
+
}
|
|
107
|
+
const slug = flag("--slug") ?? slugFrom(basename(cwd));
|
|
108
|
+
let title = flag("--title");
|
|
109
|
+
if (!title) title = titleFrom(await readFile(join(cwd, "deck.mdx"), "utf8").catch(() => ""));
|
|
110
|
+
const outDir = resolve(cwd, ".kadal-deck", "publish");
|
|
111
|
+
out(`publish @${workspace}/${slug} → ${host}`);
|
|
112
|
+
const result = await publish({
|
|
113
|
+
host,
|
|
114
|
+
token: creds.token,
|
|
115
|
+
workspace,
|
|
116
|
+
slug,
|
|
117
|
+
title,
|
|
118
|
+
outDir,
|
|
119
|
+
log: out,
|
|
120
|
+
build: async (base, dir) => {
|
|
121
|
+
await rm(dir, {
|
|
122
|
+
recursive: true,
|
|
123
|
+
force: true
|
|
124
|
+
});
|
|
125
|
+
await runVite(cwd, [
|
|
126
|
+
"build",
|
|
127
|
+
"--base",
|
|
128
|
+
base,
|
|
129
|
+
"--outDir",
|
|
130
|
+
dir,
|
|
131
|
+
"--emptyOutDir"
|
|
132
|
+
]);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
out(`\nPublished version ${result.version}: ${result.files} files (${formatBytes(result.bytes)}), ${result.uploaded} uploaded.`);
|
|
136
|
+
out(result.url);
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
export { HELP, main };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { HostError, request } from "./host.js";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
//#region src/cli/login.ts
|
|
4
|
+
async function login(o) {
|
|
5
|
+
const log = o.log ?? (() => {});
|
|
6
|
+
const { data: s } = await request(o.host, "/cli/start", {
|
|
7
|
+
method: "POST",
|
|
8
|
+
json: {},
|
|
9
|
+
fetch: o.fetch
|
|
10
|
+
});
|
|
11
|
+
log(`Your code: ${s.code}`);
|
|
12
|
+
log(`Approve it at ${s.verify_url}`);
|
|
13
|
+
(o.open ?? openBrowser)(s.verify_url);
|
|
14
|
+
const every = o.intervalMs ?? Math.max(1, s.interval ?? 2) * 1e3;
|
|
15
|
+
const deadline = Date.now() + (s.expires_in ?? 600) * 1e3;
|
|
16
|
+
const query = `?code=${encodeURIComponent(s.code)}&poll=${encodeURIComponent(s.poll)}`;
|
|
17
|
+
while (Date.now() < deadline) {
|
|
18
|
+
await new Promise((r) => setTimeout(r, every));
|
|
19
|
+
let answer;
|
|
20
|
+
try {
|
|
21
|
+
answer = await request(o.host, `/cli/poll${query}`, {
|
|
22
|
+
accept: [202],
|
|
23
|
+
fetch: o.fetch
|
|
24
|
+
});
|
|
25
|
+
} catch (e) {
|
|
26
|
+
if (e instanceof HostError && (e.status === 429 || e.status >= 500)) continue;
|
|
27
|
+
throw e;
|
|
28
|
+
}
|
|
29
|
+
if (answer.status === 202 || !answer.data?.token) continue;
|
|
30
|
+
return {
|
|
31
|
+
host: o.host,
|
|
32
|
+
token: answer.data.token,
|
|
33
|
+
...answer.data.workspace?.handle ? { workspace: answer.data.workspace.handle } : {}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
throw new Error("The login code expired before it was approved; run `kadal-deck login` again.");
|
|
37
|
+
}
|
|
38
|
+
/** Best effort: if no browser opens, the URL is already on screen. */
|
|
39
|
+
function openBrowser(url) {
|
|
40
|
+
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", [
|
|
41
|
+
"/c",
|
|
42
|
+
"start",
|
|
43
|
+
"",
|
|
44
|
+
url
|
|
45
|
+
]] : ["xdg-open", [url]];
|
|
46
|
+
try {
|
|
47
|
+
const child = spawn(cmd, args, {
|
|
48
|
+
stdio: "ignore",
|
|
49
|
+
detached: true
|
|
50
|
+
});
|
|
51
|
+
child.on("error", () => {});
|
|
52
|
+
child.unref();
|
|
53
|
+
} catch {}
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
export { login, openBrowser };
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { HostError, request } from "./host.js";
|
|
2
|
+
import { basename, extname, join, relative, sep } from "node:path";
|
|
3
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
//#region src/cli/publish.ts
|
|
6
|
+
async function publish(o) {
|
|
7
|
+
const log = o.log ?? (() => {});
|
|
8
|
+
const http = {
|
|
9
|
+
token: o.token,
|
|
10
|
+
fetch: o.fetch
|
|
11
|
+
};
|
|
12
|
+
const { data: deck } = await request(o.host, `/workspaces/${encodeURIComponent(o.workspace)}/decks`, {
|
|
13
|
+
...http,
|
|
14
|
+
json: {
|
|
15
|
+
slug: o.slug,
|
|
16
|
+
...o.title ? { title: o.title } : {}
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
log(`deck ${deck.id} ${deck.base}`);
|
|
20
|
+
await o.build(deck.base, o.outDir);
|
|
21
|
+
const files = await manifestOf(o.outDir);
|
|
22
|
+
if (!files.some((f) => f.path === "index.html")) throw new Error(`The build in ${o.outDir} has no index.html — is this a deck folder?`);
|
|
23
|
+
const bytes = files.reduce((n, f) => n + f.size, 0);
|
|
24
|
+
log(`manifest ${files.length} files, ${formatBytes(bytes)}`);
|
|
25
|
+
const { data: created } = await request(o.host, `/decks/${encodeURIComponent(deck.id)}/versions`, {
|
|
26
|
+
...http,
|
|
27
|
+
json: { files }
|
|
28
|
+
});
|
|
29
|
+
const bySha = new Map(files.map((f) => [f.sha256, f]));
|
|
30
|
+
const upload = async (shas) => {
|
|
31
|
+
const queue = shas.map((s) => bySha.get(s)).filter((f) => !!f);
|
|
32
|
+
let next = 0;
|
|
33
|
+
const worker = async () => {
|
|
34
|
+
while (next < queue.length) {
|
|
35
|
+
const f = queue[next++];
|
|
36
|
+
const body = new Uint8Array(await readFile(join(o.outDir, ...f.path.split("/"))));
|
|
37
|
+
await withRetries(() => request(o.host, `/blobs/${f.sha256}`, {
|
|
38
|
+
...http,
|
|
39
|
+
method: "PUT",
|
|
40
|
+
body,
|
|
41
|
+
contentType: f.type
|
|
42
|
+
}), o.retries ?? 3);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
await Promise.all(Array.from({ length: Math.min(o.concurrency ?? 6, queue.length) }, worker));
|
|
46
|
+
return queue.reduce((n, f) => n + f.size, 0);
|
|
47
|
+
};
|
|
48
|
+
let uploadedBytes = await upload(created.missing);
|
|
49
|
+
log(`upload ${created.missing.length} new file(s), ${formatBytes(uploadedBytes)}`);
|
|
50
|
+
const commit = () => request(o.host, `/decks/${encodeURIComponent(deck.id)}/versions/${created.version}/commit`, {
|
|
51
|
+
...http,
|
|
52
|
+
method: "POST",
|
|
53
|
+
json: {}
|
|
54
|
+
});
|
|
55
|
+
let done;
|
|
56
|
+
try {
|
|
57
|
+
done = (await commit()).data;
|
|
58
|
+
} catch (e) {
|
|
59
|
+
const missing = e.status === 409 ? missingOf(e.body) : [];
|
|
60
|
+
if (!missing.length) throw e;
|
|
61
|
+
log(`retry the host is missing ${missing.length} file(s); sending them again`);
|
|
62
|
+
uploadedBytes += await upload(missing);
|
|
63
|
+
done = (await commit()).data;
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
deck,
|
|
67
|
+
version: created.version,
|
|
68
|
+
url: done.url,
|
|
69
|
+
files: files.length,
|
|
70
|
+
uploaded: created.missing.length,
|
|
71
|
+
bytes,
|
|
72
|
+
uploadedBytes
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function missingOf(body) {
|
|
76
|
+
const m = body?.missing;
|
|
77
|
+
return Array.isArray(m) ? m.filter((s) => typeof s === "string") : [];
|
|
78
|
+
}
|
|
79
|
+
/** Retry network failures and 5xx/429 answers with backoff; anything else is the caller's error. */
|
|
80
|
+
async function withRetries(fn, retries) {
|
|
81
|
+
for (let attempt = 0;; attempt++) try {
|
|
82
|
+
return await fn();
|
|
83
|
+
} catch (e) {
|
|
84
|
+
const status = e instanceof HostError ? e.status : 0;
|
|
85
|
+
if (!(status === 0 || status === 429 || status >= 500) || attempt >= retries) throw e;
|
|
86
|
+
await new Promise((r) => setTimeout(r, 400 * 2 ** attempt));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Every file under `dir`, with forward-slash paths, sizes, SHA-256 and a content type. */
|
|
90
|
+
async function manifestOf(dir) {
|
|
91
|
+
const out = [];
|
|
92
|
+
const walk = async (d) => {
|
|
93
|
+
for (const entry of await readdir(d, { withFileTypes: true })) {
|
|
94
|
+
const full = join(d, entry.name);
|
|
95
|
+
if (entry.isDirectory()) await walk(full);
|
|
96
|
+
else if (entry.isFile()) {
|
|
97
|
+
const data = await readFile(full);
|
|
98
|
+
out.push({
|
|
99
|
+
path: relative(dir, full).split(sep).join("/"),
|
|
100
|
+
size: (await stat(full)).size,
|
|
101
|
+
sha256: createHash("sha256").update(data).digest("hex"),
|
|
102
|
+
type: typeOf(full)
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
await walk(dir);
|
|
108
|
+
return out.sort((a, b) => a.path.localeCompare(b.path));
|
|
109
|
+
}
|
|
110
|
+
const TYPES = {
|
|
111
|
+
".html": "text/html; charset=utf-8",
|
|
112
|
+
".js": "text/javascript; charset=utf-8",
|
|
113
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
114
|
+
".css": "text/css; charset=utf-8",
|
|
115
|
+
".json": "application/json",
|
|
116
|
+
".map": "application/json",
|
|
117
|
+
".svg": "image/svg+xml",
|
|
118
|
+
".png": "image/png",
|
|
119
|
+
".jpg": "image/jpeg",
|
|
120
|
+
".jpeg": "image/jpeg",
|
|
121
|
+
".gif": "image/gif",
|
|
122
|
+
".webp": "image/webp",
|
|
123
|
+
".avif": "image/avif",
|
|
124
|
+
".ico": "image/x-icon",
|
|
125
|
+
".woff": "font/woff",
|
|
126
|
+
".woff2": "font/woff2",
|
|
127
|
+
".ttf": "font/ttf",
|
|
128
|
+
".otf": "font/otf",
|
|
129
|
+
".wasm": "application/wasm",
|
|
130
|
+
".mp4": "video/mp4",
|
|
131
|
+
".webm": "video/webm",
|
|
132
|
+
".mp3": "audio/mpeg",
|
|
133
|
+
".txt": "text/plain; charset=utf-8",
|
|
134
|
+
".xml": "application/xml",
|
|
135
|
+
".pdf": "application/pdf",
|
|
136
|
+
".py": "text/plain; charset=utf-8",
|
|
137
|
+
".excalidraw": "application/json"
|
|
138
|
+
};
|
|
139
|
+
function typeOf(path) {
|
|
140
|
+
return TYPES[extname(path).toLowerCase()] ?? "application/octet-stream";
|
|
141
|
+
}
|
|
142
|
+
/** A slug the host accepts (lowercase letters, digits, hyphens; max 64), from a folder name. */
|
|
143
|
+
function slugFrom(name) {
|
|
144
|
+
const s = basename(name).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, 64);
|
|
145
|
+
return /^[a-z0-9]/.test(s) ? s : `deck-${s}`.slice(0, 64).replace(/-+$/, "") || "deck";
|
|
146
|
+
}
|
|
147
|
+
/** The `title:` from deck.mdx's headmatter, when there is one. */
|
|
148
|
+
function titleFrom(mdx) {
|
|
149
|
+
const head = /^---\r?\n([\s\S]*?)\r?\n---/.exec(mdx)?.[1];
|
|
150
|
+
const raw = head ? /^title:\s*(.+)$/m.exec(head)?.[1]?.trim() : void 0;
|
|
151
|
+
return raw ? raw.replace(/^(["'])(.*)\1$/, "$2").slice(0, 140) : void 0;
|
|
152
|
+
}
|
|
153
|
+
function formatBytes(n) {
|
|
154
|
+
if (n < 1024) return `${n} B`;
|
|
155
|
+
if (n < 1048576) return `${(n / 1024).toFixed(1)} kB`;
|
|
156
|
+
return `${(n / 1024 / 1024).toFixed(2)} MB`;
|
|
157
|
+
}
|
|
158
|
+
//#endregion
|
|
159
|
+
export { formatBytes, manifestOf, publish, slugFrom, titleFrom, typeOf };
|
package/dist/cli/vite.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
//#region src/cli/vite.ts
|
|
6
|
+
/**
|
|
7
|
+
* The deck's own Vite, run with whatever runtime is running us (Node or Bun). Resolved from the
|
|
8
|
+
* deck folder, so a deck uses the Vite version it installed rather than one we brought along.
|
|
9
|
+
*/
|
|
10
|
+
function viteBin(cwd) {
|
|
11
|
+
const req = createRequire(join(cwd, "package.json"));
|
|
12
|
+
let pkgPath;
|
|
13
|
+
try {
|
|
14
|
+
pkgPath = req.resolve("vite/package.json");
|
|
15
|
+
} catch {
|
|
16
|
+
throw new Error(`Vite isn't installed in ${cwd}. Run \`npm install\` in the deck folder first.`);
|
|
17
|
+
}
|
|
18
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
19
|
+
const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.vite ?? "bin/vite.js";
|
|
20
|
+
const bin = join(dirname(pkgPath), rel);
|
|
21
|
+
if (!existsSync(bin)) throw new Error(`Couldn't find Vite's CLI at ${bin}.`);
|
|
22
|
+
return bin;
|
|
23
|
+
}
|
|
24
|
+
function spawnVite(cwd, args, stdio = "inherit") {
|
|
25
|
+
return spawn(process.execPath, [viteBin(cwd), ...args], {
|
|
26
|
+
cwd,
|
|
27
|
+
stdio,
|
|
28
|
+
env: process.env
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function runVite(cwd, args) {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const child = spawnVite(cwd, args);
|
|
34
|
+
child.on("error", reject);
|
|
35
|
+
child.on("exit", (code, signal) => code === 0 ? resolve() : reject(/* @__PURE__ */ new Error(`vite ${args[0] ?? ""} failed (${signal ?? `exit ${code}`})`)));
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { runVite, spawnVite, viteBin };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/compiler/code-meta.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Code fence meta, the Slidev way with reveal.js's spellings accepted too:
|
|
4
|
+
*
|
|
5
|
+
* ```ts {2-3|5|all} highlight steps, one per click
|
|
6
|
+
* ```ts {2-3|5|all}{lines:true} + options: lines, startLine, maxHeight
|
|
7
|
+
* ```ts [app.ts] a title bar with the file name
|
|
8
|
+
* ```ts {*}{maxHeight:'200px'} `*` and `all` both mean every line, `none` means no highlight
|
|
9
|
+
* ```ts data-line-numbers="3-5|8" reveal.js's attribute form
|
|
10
|
+
*
|
|
11
|
+
* A step is a set of line numbers; `null` inside `steps` means "all lines" and an empty set means
|
|
12
|
+
* "no lines". A fence with N steps registers N-1 clicks on its slide.
|
|
13
|
+
*/
|
|
14
|
+
export type LineSet = Set<number> | null;
|
|
15
|
+
export interface CodeMeta {
|
|
16
|
+
/** One entry per click step; a single entry means a static highlight. Absent → no highlighting. */
|
|
17
|
+
steps: LineSet[] | null;
|
|
18
|
+
/** Show line numbers. */
|
|
19
|
+
lines: boolean;
|
|
20
|
+
/** First line number shown. */
|
|
21
|
+
startLine: number;
|
|
22
|
+
title: string | null;
|
|
23
|
+
/** CSS length; the block scrolls beyond it. */
|
|
24
|
+
maxHeight: string | null;
|
|
25
|
+
/** Hide the whole block until its first step (Slidev's `{hide|...}`). */
|
|
26
|
+
hideUntilStep: boolean;
|
|
27
|
+
}
|
|
28
|
+
export declare function parseLineSet(spec: string, total?: number): LineSet;
|
|
29
|
+
export declare function parseCodeMeta(meta: string | null | undefined): CodeMeta;
|
|
30
|
+
/** Serialise steps for a `data-steps` attribute: `2,3|5|` (empty = all). */
|
|
31
|
+
export declare function serialiseSteps(steps: LineSet[]): string;
|
|
32
|
+
export declare function deserialiseSteps(text: string): LineSet[];
|
|
33
|
+
//#endregion
|