deck-plugin-sdk 1.2.0 → 1.5.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/README.md CHANGED
@@ -1,77 +1,96 @@
1
- # deck-plugin-sdk
2
-
3
- Build a plugin for [Deck](https://deck.hasan-zarif.com), outside Deck's own repository.
4
-
5
- ```sh
6
- npm create deck-plugin my-thing
7
- cd my-thing
8
- npm run dev # build, and rebuild as you edit
9
- npm run install-local # copy it into Deck, which loads it without a restart
10
- ```
11
-
12
- ## What a plugin is
13
-
14
- A folder with a `plugin.json` and a built `plugin.js`, read from
15
- `%APPDATA%\Deck\plugins\<id>\` when Deck starts. It becomes a tab, or a readout in the footer,
16
- or a panel, or commands in the palette — `plugins/API.md` in the Deck repository lists every
17
- slot a plugin can fill. One is enough.
18
-
19
- Your sources import `react`, `lucide-react` and the shim modules as ordinary code does. At run
20
- time those resolve to **Deck's** copies, handed over on `window.__deck`. For React that is not
21
- an optimisation but a requirement: two Reacts in one tree break hooks.
22
-
23
- ## Commands
24
-
25
- | | |
26
- |---|---|
27
- | `deck-plugin build` | bundle `src/` into `plugin.js` |
28
- | `deck-plugin dev` | the same, rebuilt whenever a source changes |
29
- | `deck-plugin check` | validate `plugin.json` without building |
30
- | `deck-plugin install` | build, then copy into Deck's plugins folder |
31
-
32
- `install` prefers `%APPDATA%\Deck-Dev` when it exists, since anyone writing a plugin is probably
33
- running a debug build beside the installed one.
34
-
35
- ## What you can import
36
-
37
- ```ts
38
- import { configRead, configWrite } from "../shim/bridge.js"; // Deck's Tauri commands
39
- import { listen } from "../shim/event.js"; // Deck's event bus
40
- import { getSettings } from "../shim/settings.js"; // Deck's settings store
41
- import { Field, GhostButton } from "../shim/ui.js"; // Deck's form controls
42
- import { getLayout } from "../shim/terminals.js"; // the terminal layout
43
- import { monaco } from "../shim/monaco.js"; // the editor
44
- ```
45
-
46
- `shim/` and `types/` are written by the build. They are not yours to edit and belong in
47
- `.gitignore` — the template already ignores them.
48
-
49
- The types are generated from Deck's own source, so what your editor tells you is what Deck
50
- actually exposes rather than a hand-written approximation that has drifted.
51
-
52
- ## What a plugin cannot do
53
-
54
- Add native capability. Every Tauri command is compiled into Deck, so a plugin calls what already
55
- exists. Anything needing new Rust belongs in Deck itself.
56
-
57
- ## What a plugin CAN do, which is worth saying plainly
58
-
59
- **Everything Deck can.** A plugin runs inside Deck with the whole bridge: every Tauri command,
60
- the config directory, the filesystem, the terminal, a Google access token if one is connected.
61
- There is no sandbox and no permission prompt.
62
-
63
- That is a deliberate position rather than an oversight — a plugin is code you chose to run, like
64
- a VS Code extension or a shell script. It is also why the plugin registry is curated rather than
65
- open: an entry exists there because it was reviewed, and installing a plugin from anywhere else
66
- is the same decision as running any other program.
67
-
68
- ## Versions
69
-
70
- The SDK's major and minor track Deck's, so an SDK 0.15 builds plugins for Deck 0.15. Declare the
71
- oldest Deck you work with as `minDeck` in `plugin.json`; Deck refuses to load a plugin that
72
- needs a newer one, with a sentence rather than an error from inside your module.
73
-
74
- ## Publishing
75
-
76
- A plugin is published by being listed in Deck's registry, which points at a release in your own
77
- repository. Until that is open to submissions, ask.
1
+ # deck-plugin-sdk
2
+
3
+ Build a plugin for Deck, outside Deck's own repository.
4
+
5
+ ```sh
6
+ npm create deck-plugin my-thing
7
+ cd my-thing
8
+ npm run dev # rebuild as you edit; each save shows in your test Deck
9
+ npm run install-local # when it is done: install it into your real Deck
10
+ ```
11
+
12
+ Develop in a **test Deck**: a second Deck beside yours, with its own settings and data, started
13
+ from Settings › Plugins › Open a test Deck (or `deck.exe --test`). `npm run dev` installs into it
14
+ after every build and it reloads the plugin, so a save shows there within a few seconds, while your
15
+ real Deck and its data are never touched.
16
+
17
+ The project comes with an `AGENTS.md` (and a `CLAUDE.md` that points to it), so Claude Code,
18
+ Codex or another agent started in the folder knows the commands, the API and the rules.
19
+
20
+ ## What a plugin is
21
+
22
+ A folder with a `plugin.json` and a built `plugin.js`, read from
23
+ `%APPDATA%\Deck\plugins\<id>\` when Deck starts. It becomes a tab, or a readout in the footer,
24
+ or a panel, or commands in the palette — `docs/guide/api.md` in the Deck repository lists every
25
+ slot a plugin can fill. One is enough.
26
+
27
+ Your sources import `react`, `lucide-react` and `deck-plugin-sdk/*` as ordinary code does. At run
28
+ time those resolve to **Deck's** copies, handed over on `window.__deck`. For React that is not
29
+ an optimisation but a requirement: two Reacts in one tree break hooks.
30
+
31
+ ## Commands
32
+
33
+ | | |
34
+ |---|---|
35
+ | `deck-plugin build` | bundle `src/` into `plugin.js` |
36
+ | `deck-plugin dev` | rebuild whenever a source changes, and install each build into the test Deck |
37
+ | `deck-plugin check` | validate `plugin.json` without building |
38
+ | `deck-plugin install` | build once, then install into your real Deck |
39
+
40
+ `--target test` or `--target main` picks the other Deck for either command. The test Deck keeps
41
+ its plugins in `%APPDATA%\Deck-Dev\plugins`, your real Deck in `%APPDATA%\Deck\plugins`.
42
+
43
+ ## What you can import
44
+
45
+ ```ts
46
+ import { configRead, configWrite } from "deck-plugin-sdk/bridge"; // Deck's Tauri commands
47
+ import { listen } from "deck-plugin-sdk/event"; // Deck's event bus
48
+ import { getSettings } from "deck-plugin-sdk/settings"; // Deck's settings store
49
+ import { Field, GhostButton } from "deck-plugin-sdk/ui"; // Deck's form controls
50
+ import { getLayout } from "deck-plugin-sdk/terminals"; // the terminal layout
51
+ import { monaco } from "deck-plugin-sdk/monaco"; // the editor
52
+ ```
53
+
54
+ They come from the package, so the project holds only your own code. A plugin made with an
55
+ older SDK imports `../shim/bridge.js` instead; the build still copies `shim/` and `types/` in
56
+ for one of those, and switching its imports over makes both unnecessary.
57
+
58
+ The types are generated from Deck's own source, so what your editor tells you is what Deck
59
+ actually exposes rather than a hand-written approximation that has drifted.
60
+
61
+ Any other npm package is `npm install` and import: the build bundles it into `plugin.js`, with the
62
+ images, fonts and CSS it brings. Writing a plugin, in the Docs, says what cannot work that way.
63
+
64
+ When the build needs more than that (a worker bundled into the plugin, a dependency's locales left
65
+ out), export esbuild plugins from `deck-plugin.config.mjs` beside `plugin.json`:
66
+
67
+ ```js
68
+ export const esbuildPlugins = [{ name: "my-step", setup(build) { /* ... */ } }];
69
+ ```
70
+
71
+ ## What a plugin cannot do
72
+
73
+ Add native capability. Every Tauri command is compiled into Deck, so a plugin calls what already
74
+ exists. Anything needing new Rust belongs in Deck itself.
75
+
76
+ ## What a plugin CAN do, which is worth saying plainly
77
+
78
+ **Everything Deck can.** A plugin runs inside Deck with the whole bridge: every Tauri command,
79
+ the config directory, the filesystem, the terminal, a Google access token if one is connected.
80
+ There is no sandbox and no permission prompt.
81
+
82
+ That is a deliberate position rather than an oversight — a plugin is code you chose to run, like
83
+ a VS Code extension or a shell script. It is also why the plugin registry is curated rather than
84
+ open: an entry exists there because it was reviewed, and installing a plugin from anywhere else
85
+ is the same decision as running any other program.
86
+
87
+ ## Versions
88
+
89
+ The SDK's major and minor track Deck's, so an SDK 0.15 builds plugins for Deck 0.15. Declare the
90
+ oldest Deck you work with as `minDeck` in `plugin.json`; Deck refuses to load a plugin that
91
+ needs a newer one, with a sentence rather than an error from inside your module.
92
+
93
+ ## Publishing
94
+
95
+ A plugin is published by being listed in Deck's registry, which points at a release in your own
96
+ repository. Until that is open to submissions, ask.
@@ -1,117 +1,150 @@
1
1
  #!/usr/bin/env node
2
- // The Deck plugin CLI.
3
- //
4
- // deck-plugin build bundle src/ into plugin.js
5
- // deck-plugin dev rebuild whenever a source changes
6
- // deck-plugin check validate plugin.json without building
7
- // deck-plugin install copy the built plugin into Deck's plugins folder
8
- //
9
- // Run from a plugin's own directory.
10
- import { watch } from "node:fs";
11
- import { cpSync, existsSync, mkdirSync, readFileSync } from "node:fs";
12
- import { join } from "node:path";
13
- import { buildPlugin } from "../build-plugin.mjs";
14
-
15
- const [cmd = "build"] = process.argv.slice(2);
16
- const dir = process.cwd();
17
-
18
- const die = (msg) => { console.error(msg); process.exit(1); };
19
-
20
- /**
21
- * What Deck will refuse at install time, refused here instead.
22
- *
23
- * Every rule is one Deck's loader or install_from_registry applies — the id charset especially,
24
- * because the id becomes a config key and a path component. Finding out here costs a second;
25
- * finding out from a failed install costs a round trip.
26
- */
27
- function check() {
28
- const p = join(dir, "plugin.json");
29
- if (!existsSync(p)) die("no plugin.json here — is this a plugin directory?");
30
-
31
- let manifest;
32
- try { manifest = JSON.parse(readFileSync(p, "utf8")); }
33
- catch (e) { die(`plugin.json is not valid JSON: ${e.message}`); }
34
-
35
- const id = (manifest.id ?? "").trim();
36
- if (!id) die("plugin.json has no id");
37
- if (!/^[a-zA-Z0-9][a-zA-Z0-9-]*$/.test(id)) {
38
- die(`id "${id}" must be letters, digits and dashes, starting with a letter or digit`);
39
- }
40
- if (!existsSync(join(dir, "src", "plugin.tsx"))) {
41
- die("no src/plugin.tsx — that is the entry point");
42
- }
43
-
44
- // Not fatal, but worth saying: the registry's plugin page renders a README, and a published
45
- // plugin without one gets an empty page.
46
- const warn = [];
47
- if (!manifest.version) warn.push("no version — Settings shows one, and the registry orders by it");
48
- if (!manifest.description) warn.push("no description — the row says only a name");
49
- if (!existsSync(join(dir, "README.md"))) warn.push("no README.md — publishing needs one");
50
-
51
- console.log(`${manifest.name ?? id} ${manifest.version ?? ""}`.trim());
52
- for (const w of warn) console.log(` note: ${w}`);
53
- return manifest;
54
- }
55
-
56
- /** Where Deck reads plugins from on this machine. */
57
- function pluginsDir() {
58
- const appData = process.env.APPDATA;
59
- if (!appData) die("APPDATA is not set — this command is for Windows, where Deck runs");
60
- // Deck-Dev is the debug build's directory; prefer it when it exists, since someone building a
61
- // plugin is almost certainly running a dev build beside it.
62
- const dev = join(appData, "Deck-Dev", "plugins");
63
- return existsSync(dev) ? dev : join(appData, "Deck", "plugins");
64
- }
65
-
66
- switch (cmd) {
67
- case "check": {
68
- check();
69
- break;
70
- }
71
-
72
- case "build": {
73
- const manifest = check();
74
- await buildPlugin(dir);
75
- console.log(`built ${manifest.id} -> plugin.js`);
76
- break;
77
- }
78
-
79
- case "install": {
80
- const manifest = check();
81
- await buildPlugin(dir);
82
- const dest = join(pluginsDir(), manifest.id);
83
- mkdirSync(dest, { recursive: true });
84
- for (const f of ["plugin.json", "plugin.js", "mcp.js"]) {
85
- if (existsSync(join(dir, f))) cpSync(join(dir, f), join(dest, f));
86
- }
87
- console.log(`installed ${manifest.id} -> ${dest}`);
88
- console.log("Deck watches that folder, so it is loaded already — no restart.");
89
- break;
90
- }
91
-
92
- case "dev": {
93
- check();
94
- let building = false;
95
- let again = false;
96
- const once = async () => {
97
- if (building) { again = true; return; }
98
- building = true;
99
- try { await buildPlugin(dir); console.log(`rebuilt at ${new Date().toLocaleTimeString()}`); }
100
- catch (e) { console.error(e.message); }
101
- building = false;
102
- if (again) { again = false; void once(); }
103
- };
104
- await once();
105
- // Debounced: an editor writing a file emits several events, and each would start a build.
106
- let timer;
107
- watch(join(dir, "src"), { recursive: true }, () => {
108
- clearTimeout(timer);
109
- timer = setTimeout(() => void once(), 120);
110
- });
111
- console.log("watching src/ — ctrl+c to stop");
112
- break;
113
- }
114
-
115
- default:
116
- die(`unknown command "${cmd}" — try build, dev, check or install`);
117
- }
2
+ // The Deck plugin CLI.
3
+ //
4
+ // deck-plugin build bundle src/ into plugin.js
5
+ // deck-plugin dev rebuild whenever a source changes, into the test Deck each time
6
+ // deck-plugin check validate plugin.json without building
7
+ // deck-plugin install build once and copy into your Deck
8
+ //
9
+ // --target test|main which Deck dev and install write to (defaults: dev -> test, install -> main)
10
+ //
11
+ // Run from a plugin's own directory.
12
+ import { watch } from "node:fs";
13
+ import { cpSync, existsSync, mkdirSync, readFileSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { buildPlugin } from "../build-plugin.mjs";
16
+
17
+ const argv = process.argv.slice(2);
18
+ const at = argv.indexOf("--target");
19
+ const targetFlag = at >= 0 ? argv[at + 1] : undefined;
20
+ const cmd = argv.find((a, i) => !a.startsWith("-") && !(at >= 0 && i === at + 1)) ?? "build";
21
+ const dir = process.cwd();
22
+
23
+ const die = (msg) => { console.error(msg); process.exit(1); };
24
+
25
+ /**
26
+ * What Deck will refuse at install time, refused here instead.
27
+ *
28
+ * Every rule is one Deck's loader or install_from_registry applies — the id charset especially,
29
+ * because the id becomes a config key and a path component. Finding out here costs a second;
30
+ * finding out from a failed install costs a round trip.
31
+ */
32
+ function check() {
33
+ const p = join(dir, "plugin.json");
34
+ if (!existsSync(p)) die("no plugin.json here — is this a plugin directory?");
35
+
36
+ let manifest;
37
+ try { manifest = JSON.parse(readFileSync(p, "utf8")); }
38
+ catch (e) { die(`plugin.json is not valid JSON: ${e.message}`); }
39
+
40
+ const id = (manifest.id ?? "").trim();
41
+ if (!id) die("plugin.json has no id");
42
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9-]*$/.test(id)) {
43
+ die(`id "${id}" must be letters, digits and dashes, starting with a letter or digit`);
44
+ }
45
+ if (!existsSync(join(dir, "src", "plugin.tsx"))) {
46
+ die("no src/plugin.tsx — that is the entry point");
47
+ }
48
+
49
+ // Not fatal, but worth saying: the registry's plugin page renders a README, and a published
50
+ // plugin without one gets an empty page.
51
+ const warn = [];
52
+ if (!manifest.version) warn.push("no version — Settings shows one, and the registry orders by it");
53
+ if (!manifest.description) warn.push("no description — the row says only a name");
54
+ if (!existsSync(join(dir, "README.md"))) warn.push("no README.md — publishing needs one");
55
+
56
+ console.log(`${manifest.name ?? id} ${manifest.version ?? ""}`.trim());
57
+ for (const w of warn) console.log(` note: ${w}`);
58
+ return manifest;
59
+ }
60
+
61
+ /**
62
+ * The plugins folder of one of the two Decks on this machine.
63
+ *
64
+ * "test" is the test Deck (`deck.exe --test`, or Settings › Plugins › Open a test Deck), which keeps
65
+ * its own folder, Deck-Dev, apart from your real data. `dev` always writes there, so a plugin you
66
+ * are still writing never lands in your real Deck, even when the test Deck is closed. `install`
67
+ * writes to your real Deck ("main"), for when the plugin is done.
68
+ */
69
+ function pluginsDir(defaultTarget) {
70
+ const appData = process.env.APPDATA;
71
+ if (!appData) die("APPDATA is not set — this command is for Windows, where Deck runs");
72
+ const target = targetFlag ?? defaultTarget;
73
+ if (target !== "test" && target !== "main") die(`--target is "test" or "main", not "${target}"`);
74
+ return { target, dir: join(appData, target === "test" ? "Deck-Dev" : "Deck", "plugins") };
75
+ }
76
+
77
+ /** Copy the built plugin into a Deck's plugins folder. Deck watches it and reloads the plugin. */
78
+ function copyInto(manifest, defaultTarget) {
79
+ const { target, dir: plugins } = pluginsDir(defaultTarget);
80
+ const dest = join(plugins, manifest.id);
81
+ mkdirSync(dest, { recursive: true });
82
+ for (const f of ["plugin.json", "plugin.js", "mcp.js"]) {
83
+ if (existsSync(join(dir, f))) cpSync(join(dir, f), join(dest, f));
84
+ }
85
+ return { target, dest };
86
+ }
87
+
88
+ /** Whether the test Deck is running: it answers on its own port. */
89
+ async function testDeckRunning() {
90
+ try { return (await fetch("http://127.0.0.1:8791/health", { signal: AbortSignal.timeout(800) })).ok; }
91
+ catch { return false; }
92
+ }
93
+
94
+ switch (cmd) {
95
+ case "check": {
96
+ check();
97
+ break;
98
+ }
99
+
100
+ case "build": {
101
+ const manifest = check();
102
+ await buildPlugin(dir);
103
+ console.log(`built ${manifest.id} -> plugin.js`);
104
+ break;
105
+ }
106
+
107
+ case "install": {
108
+ const manifest = check();
109
+ await buildPlugin(dir);
110
+ const { target, dest } = copyInto(manifest, "main");
111
+ console.log(`installed ${manifest.id} into your ${target === "test" ? "test " : ""}Deck -> ${dest}`);
112
+ console.log("Deck watches that folder, so it is loaded already — no restart.");
113
+ break;
114
+ }
115
+
116
+ case "dev": {
117
+ const manifest = check();
118
+ let building = false;
119
+ let again = false;
120
+ const once = async () => {
121
+ if (building) { again = true; return; }
122
+ building = true;
123
+ try {
124
+ await buildPlugin(dir);
125
+ // Into the test Deck after every build: it reloads the plugin when its folder changes, so
126
+ // a save shows there within a few seconds.
127
+ const { target, dest } = copyInto(manifest, "test");
128
+ console.log(`rebuilt at ${new Date().toLocaleTimeString()} -> ${target} Deck (${dest})`);
129
+ } catch (e) { console.error(e.message); }
130
+ building = false;
131
+ if (again) { again = false; void once(); }
132
+ };
133
+ if ((targetFlag ?? "test") === "test" && !(await testDeckRunning())) {
134
+ console.log("No test Deck is running, so you will not see changes yet. Start one from");
135
+ console.log("Settings › Plugins › Open a test Deck (or run deck.exe --test); it loads this plugin.");
136
+ }
137
+ await once();
138
+ // Debounced: an editor writing a file emits several events, and each would start a build.
139
+ let timer;
140
+ watch(join(dir, "src"), { recursive: true }, () => {
141
+ clearTimeout(timer);
142
+ timer = setTimeout(() => void once(), 120);
143
+ });
144
+ console.log("watching src/ — ctrl+c to stop");
145
+ break;
146
+ }
147
+
148
+ default:
149
+ die(`unknown command "${cmd}" — try build, dev, check or install`);
150
+ }
package/build-plugin.mjs CHANGED
@@ -13,16 +13,20 @@
13
13
  // DECK'S React — which is not an optimisation but a requirement, since two Reacts in one tree
14
14
  // break hooks.
15
15
  import { build } from "esbuild";
16
- import { cpSync, existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
16
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
17
17
  import { createRequire } from "node:module";
18
18
  import { dirname, join } from "node:path";
19
- import { fileURLToPath } from "node:url";
19
+ import { fileURLToPath, pathToFileURL } from "node:url";
20
+ import { assetLoaders, fromPackage, packageCss, rawImports } from "./packages.mjs";
20
21
 
21
22
  const sdk = dirname(fileURLToPath(import.meta.url));
22
23
  const require = createRequire(import.meta.url);
23
24
 
24
- /** The module names a plugin may import, and the shipped file each resolves to. */
25
- function shimMap(shimDir) {
25
+ /**
26
+ * The module names a plugin may import, and the file each resolves to: shipped ones in `shimDir`,
27
+ * the two generated per plugin in `genDir`.
28
+ */
29
+ function shimMap(shimDir, genDir) {
26
30
  return {
27
31
  react: join(shimDir, "react.js"),
28
32
  "deckreact/jsx-runtime": join(shimDir, "jsx-runtime.js"),
@@ -31,17 +35,21 @@ function shimMap(shimDir) {
31
35
  // Deck's react-dom (needs Deck 1.1); only bundled when a plugin or its library imports it.
32
36
  "react-dom": join(shimDir, "react-dom.js"),
33
37
  "@tauri-apps/api/event": join(shimDir, "event.js"),
34
- "lucide-react": join(shimDir, "icons.js"),
35
- "framer-motion": join(shimDir, "motion.js"),
38
+ "lucide-react": join(genDir, "icons.js"),
39
+ "framer-motion": join(genDir, "motion.js"),
36
40
  };
37
41
  }
38
42
 
39
43
  /**
40
44
  * Build the plugin in `dir`.
41
45
  *
42
- * The shim is copied in rather than imported from node_modules, because a plugin's sources refer
43
- * to it by relative path (`../shim/bridge.js`) and that has to resolve on disk. Copied on every
44
- * build so updating the SDK updates the shim, which is the behaviour someone upgrading expects.
46
+ * A plugin imports the shim from this package (`deck-plugin-sdk/bridge`), so its project holds
47
+ * only its own code: the editor and esbuild both find the shim and its types in node_modules.
48
+ * The two files generated per plugin go to node_modules/.cache, for the length of a build.
49
+ *
50
+ * A project made before that imports `../shim/bridge.js`, a path that has to exist on disk, so
51
+ * for one of those the shim and its types are still copied in beside `src/`, on every build so
52
+ * updating the SDK updates them.
45
53
  */
46
54
  export async function buildPlugin(dir, { entry = "plugin.tsx" } = {}) {
47
55
  const manifestPath = join(dir, "plugin.json");
@@ -50,14 +58,31 @@ export async function buildPlugin(dir, { entry = "plugin.tsx" } = {}) {
50
58
  }
51
59
  const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
52
60
 
53
- const shimDir = join(dir, "shim");
54
- rmSync(shimDir, { recursive: true, force: true });
55
- cpSync(join(sdk, "shim"), shimDir, { recursive: true });
56
- // The declarations the shim's .d.ts files point at, as ../types/. Copied beside it for the
57
- // same reason: a relative reference has to find something.
58
- const typesDir = join(dir, "types");
59
- rmSync(typesDir, { recursive: true, force: true });
60
- cpSync(join(sdk, "types"), typesDir, { recursive: true });
61
+ // A plugin that needs more from esbuild than this build gives (a worker bundled into it, a
62
+ // dependency's locales left out) exports `esbuildPlugins` from deck-plugin.config.mjs beside
63
+ // plugin.json. They run after Deck's own, so they never take over react or the shim.
64
+ const configPath = join(dir, "deck-plugin.config.mjs");
65
+ const config = existsSync(configPath) ? await import(pathToFileURL(configPath).href) : {};
66
+
67
+ const sources = readdirSync(join(dir, "src"), { recursive: true })
68
+ .filter((f) => typeof f === "string" && /\.tsx?$/.test(f))
69
+ .map((f) => readFileSync(join(dir, "src", f), "utf8"));
70
+
71
+ const legacy = sources.some((src) => /from\s*["'](\.\.\/)+shim\//.test(src));
72
+ let shimDir = join(sdk, "shim");
73
+ let genDir = join(dir, "node_modules", ".cache", "deck-plugin");
74
+ if (legacy) {
75
+ shimDir = genDir = join(dir, "shim");
76
+ rmSync(shimDir, { recursive: true, force: true });
77
+ cpSync(join(sdk, "shim"), shimDir, { recursive: true });
78
+ // The declarations the shim's .d.ts files point at, as ../types/. Copied beside it for the
79
+ // same reason: a relative reference has to find something.
80
+ const typesDir = join(dir, "types");
81
+ rmSync(typesDir, { recursive: true, force: true });
82
+ cpSync(join(sdk, "types"), typesDir, { recursive: true });
83
+ } else {
84
+ mkdirSync(genDir, { recursive: true });
85
+ }
61
86
 
62
87
  // ---- icons and animation, generated per plugin ----
63
88
  //
@@ -68,10 +93,6 @@ export async function buildPlugin(dir, { entry = "plugin.tsx" } = {}) {
68
93
  //
69
94
  // Nothing about this needs Deck's source, only the plugin's own and the real package, so it
70
95
  // runs here the same way it runs inside the Deck repository.
71
- const sources = readdirSync(join(dir, "src"), { recursive: true })
72
- .filter((f) => typeof f === "string" && /\.tsx?$/.test(f))
73
- .map((f) => readFileSync(join(dir, "src", f), "utf8"));
74
-
75
96
  for (const [pkg, file, global] of [
76
97
  ["lucide-react", "icons.js", "icons"],
77
98
  ["framer-motion", "motion.js", "motion"],
@@ -90,7 +111,7 @@ export async function buildPlugin(dir, { entry = "plugin.tsx" } = {}) {
90
111
  for (const n of names) if (!(n in real)) throw new Error(`${pkg} has no export "${n}"`);
91
112
  }
92
113
 
93
- writeFileSync(join(shimDir, file), `// GENERATED by the Deck plugin build — do not edit.
114
+ writeFileSync(join(genDir, file), `// GENERATED by the Deck plugin build — do not edit.
94
115
  //
95
116
  // Deck's ${pkg}, so this plugin does not bundle its own copy. Only the names this plugin
96
117
  // imports: naming every export would defeat tree-shaking and make the bundle larger.
@@ -99,7 +120,7 @@ ${names.map((n) => `export const ${n} = __src.${n};`).join("\n")}
99
120
  `);
100
121
  }
101
122
 
102
- const shims = shimMap(shimDir);
123
+ const shims = shimMap(shimDir, genDir);
103
124
 
104
125
  await build({
105
126
  entryPoints: [join(dir, "src", entry)],
@@ -118,7 +139,8 @@ ${names.map((n) => `export const ${n} = __src.${n};`).join("\n")}
118
139
  // A .wasm import becomes a Uint8Array inlined in the bundle rather than a sidecar file. A
119
140
  // plugin is installed as plugin.json + plugin.js, so a file beside them would never arrive.
120
141
  // A .css import becomes its text, for the plugin to inject as a <style>, for the same reason.
121
- loader: { ".wasm": "binary", ".css": "text" },
142
+ // Images and fonts become data: URLs, for the same reason (packages.mjs).
143
+ loader: { ".wasm": "binary", ".css": "text", ...assetLoaders },
122
144
  // A resolve plugin rather than esbuild's `alias`, which maps package to package while these
123
145
  // point at single files.
124
146
  plugins: [{
@@ -126,9 +148,9 @@ ${names.map((n) => `export const ${n} = __src.${n};`).join("\n")}
126
148
  setup(b) {
127
149
  const filter = new RegExp(`^(${Object.keys(shims)
128
150
  .map((k) => k.replace(/[/\\^$*+?.()|[\]{}]/g, "\\$&")).join("|")})$`);
129
- b.onResolve({ filter }, (a) => ({ path: shims[a.path] }));
151
+ b.onResolve({ filter }, (a) => (fromPackage(a, dir) ? undefined : { path: shims[a.path] }));
130
152
  },
131
- }],
153
+ }, rawImports, packageCss(manifest.id, dir), ...(config.esbuildPlugins ?? [])],
132
154
  logLevel: "info",
133
155
  });
134
156
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deck-plugin-sdk",
3
- "version": "1.2.0",
3
+ "version": "1.5.0",
4
4
  "description": "Build a plugin for Deck, outside the Deck repository.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -9,6 +9,10 @@
9
9
  },
10
10
  "exports": {
11
11
  "./build": "./build-plugin.mjs",
12
+ "./*": {
13
+ "types": "./shim/*.d.ts",
14
+ "default": "./shim/*.js"
15
+ },
12
16
  "./package.json": "./package.json"
13
17
  },
14
18
  "files": [
@@ -17,6 +21,7 @@
17
21
  "types",
18
22
  "template",
19
23
  "build-plugin.mjs",
24
+ "packages.mjs",
20
25
  "README.md"
21
26
  ],
22
27
  "dependencies": {