figma-plugin-utilities 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 CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.6.0] - 2026-10-10
6
+
7
+ ### Added
8
+ - `figma-plugin-utilities/vite` — `figmaPluginConfig(import.meta.url)`, a plugin's whole Vite config: each thread built on its own (`vite build && vite build --mode code`), so both may import the same module, the UI's CSS and JS inlined into `dist/index.html`, `src/manifest.json` copied, at `es2017`. `moduleScript` inlines the UI as a module script. `ui3InlineSvg` and `inlineFigmaHtml` are exported too. `vite` and `@sveltejs/vite-plugin-svelte` are optional peer dependencies
9
+ - **EmptyState** — `iconSize`, the icon's size in px (24 by default), for an icon given as SVG markup
10
+ - **FieldGroup** — `hint`, a line of secondary text under the control: what to enter, or what the choice does. Empty shows none, so a conditional hint is a string; the `hint` slot takes markup and always shows. Like the label, it can't be selected. The hint and the control's error take the label's size: body-medium, or body-small in a small group. The Figma component has the hint too, which Code Connect and the mockup builder read
11
+
12
+ ### Changed
13
+ - **figma-ui3-kit-svelte** peer is `^0.8.0`, whose Input, Textarea, Dropdown and Dropzone size their error by the `--field-error-*` properties FieldGroup sets
14
+ - `resizeToFit` — measures only the `container` you pass, and without one or a `height` warns and leaves the window as it is. It measured `document.body` by default, which fills the window, so the window could grow but never shrink
15
+
16
+ ### Fixed
17
+ - `sendToUI`, `sendToPlugin` — a `type` field in the data no longer replaces the message's type
18
+ - `formatErrorMessage` — the network, CORS and JSON messages follow the copy guidelines: no "Please" or "Network error:" prefix
19
+ - **FieldGroup** — `labelFor` ties the label to its control, so clicking the label focuses an `Input` or `Textarea`. It was passed to Label as `for`, which Label doesn't take, so no label was tied to anything
20
+ - **EmptyState** — `icon`, `action` and `actions` have types, so a plugin's `svelte-check` with `checkJs` passes
21
+ - `autoResize` — the options default to `{ container: null }`, which matches their type; without a container it still warns and does nothing
22
+
23
+ ## [0.5.1] - 2026-10-07
24
+
25
+ ### Changed
26
+ - **figma-ui3-kit-svelte** is a peer dependency, `^0.7.0`: install it beside the utilities. 0.7.0 is what `confirmDiscardChanges` needs for Modal's `beforeClose`, and **ConfirmModal** over another modal for Escape to close only the top one
27
+
5
28
  ## [0.5.0] - 2026-10-07
6
29
 
7
30
  ### Added
@@ -56,7 +79,7 @@
56
79
  - The dev-mode `console.warn` **FieldGroup** logged when `label` was set without `labelFor` — a `Dropdown` is a button and cannot be a `<label for>` target, so it fired on correct code. Dropped in the a11y pass, recorded late
57
80
 
58
81
  ### Fixed
59
- - **StatusBar** — the default `info` type sets `color: var(--figma-color-text)`. The `error`, `success` and `warning` types each set a foreground; the default one relied on inheritance, and nothing up the tree sets `color`, so the message rendered in the UA's black on the dark theme's grey bar
82
+ - **StatusBar** — the default `info` type sets `color: var(--figma-color-text)`. The `error`, `success` and `warning` types each set a foreground; the default one relied on inheritance, and nothing up the tree sets `color`, so the message rendered in the UA's black on the dark theme's gray bar
60
83
  - **EmptyState** — the actions are a keyed `{#each}`, so swapping one action for another reuses the right button rather than repainting the row
61
84
  - **docs** — `figma-frame-builders` is documented, `sanitizeInput` no longer claims to escape HTML (it stringifies, truncates, strips control characters and trims), and `formatErrorMessage`, `handleAsyncError`, `withErrorHandling` and `logError` are documented with their real signatures. `withErrorHandling(fn, operation)` calls `fn()` with no arguments and returns its result; it was documented as returning a wrapped function
62
85
 
package/README.md CHANGED
@@ -5,9 +5,11 @@ Shared Svelte components and utilities for Figma plugins.
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- npm install figma-plugin-utilities
8
+ npm install figma-plugin-utilities figma-ui3-kit-svelte
9
9
  ```
10
10
 
11
+ The components are built from `figma-ui3-kit-svelte`, a peer dependency: install it beside the utilities.
12
+
11
13
  ## Usage
12
14
 
13
15
  ### Import Everything
@@ -78,10 +80,10 @@ import { sendToPlugin, createMessageHandler } from "figma-plugin-utilities/lib";
78
80
  | `Header` | Header bar with `left`, `center`, `right` slots and optional title |
79
81
  | `Footer` | Footer with `right`, `split`, and `full` layout variants |
80
82
  | `StatusBar` | Toast notifications with auto-dismiss (info/success/error/warning) |
81
- | `EmptyState` | Empty/error states with optional icon and action buttons; `size`, `centered`, and `role="alert"` for failures |
83
+ | `EmptyState` | Empty/error states with optional icon and action buttons; `size`, `iconSize`, `centered`, and `role="alert"` for failures |
82
84
  | `ListItem` | Selectable list items with metadata and `badge` slots, an action menu (`menuOpen`, `menuToggle`, `menuClose`) |
83
- | `LoadingState` | Centred message as `role="status"` (text only, no spinner) |
84
- | `FieldGroup` | Label + input wrapper; `labelFor` binds the label to a text control |
85
+ | `LoadingState` | Centered message as `role="status"` (text only, no spinner) |
86
+ | `FieldGroup` | Label + input wrapper; `labelFor` binds the label to a text control, `hint` adds a line under it |
85
87
  | `CheckboxCard` | Large checkbox with card styling and better touch targets; `change` event |
86
88
  | `Section` | Titled group of fields as in Figma's panels: a `Header` with the title and an `actions` slot, content padded by the section itself |
87
89
  | `DataTable` | Named rows with a cell per column (a set at each breakpoint, a style before and after): columns with their own width and alignment, a read-only mode with table roles, row selection with an `editor` slot, notes as badges, with tooltips, and a `+N` count past two, an `action` slot, values as badges or variable chips, removed rows and a marked column |
@@ -377,8 +379,8 @@ const parsed = parseJsonSafe(jsonString);
377
379
  Utilities for dynamically resizing the plugin window to fit its content.
378
380
 
379
381
  ```javascript
380
- // One-time resize to fit content
381
- resizeToFit({ width: 300, minHeight: 100, maxHeight: 600 });
382
+ // One-time resize to fit content: measure a naturally-flowing wrapper, not document.body
383
+ resizeToFit({ container: myContainerEl, width: 300, minHeight: 100, maxHeight: 600 });
382
384
 
383
385
  // Watch for content changes and auto-resize
384
386
  const cleanup = autoResize({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figma-plugin-utilities",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Shared Svelte components and utilities for Figma plugins",
5
5
  "type": "module",
6
6
  "svelte": "./src/index.js",
@@ -58,6 +58,11 @@
58
58
  "types": "./src/lib/confirm.ts",
59
59
  "import": "./src/lib/confirm.ts",
60
60
  "default": "./src/lib/confirm.ts"
61
+ },
62
+ "./vite": {
63
+ "types": "./src/vite.d.ts",
64
+ "import": "./src/vite.js",
65
+ "default": "./src/vite.js"
61
66
  }
62
67
  },
63
68
  "files": [
@@ -84,6 +89,19 @@
84
89
  ],
85
90
  "author": "Marius Roosendaal",
86
91
  "license": "MIT",
92
+ "peerDependencies": {
93
+ "figma-ui3-kit-svelte": "^0.8.0",
94
+ "@sveltejs/vite-plugin-svelte": "^3.0.2",
95
+ "vite": "^5.2.0"
96
+ },
97
+ "peerDependenciesMeta": {
98
+ "@sveltejs/vite-plugin-svelte": {
99
+ "optional": true
100
+ },
101
+ "vite": {
102
+ "optional": true
103
+ }
104
+ },
87
105
  "devDependencies": {
88
106
  "@eslint/js": "^9.39.2",
89
107
  "@figma/plugin-typings": "^1.138.0",
@@ -91,7 +109,7 @@
91
109
  "@types/node": "^22.13.4",
92
110
  "eslint": "^9.39.2",
93
111
  "eslint-plugin-svelte": "^3.23.0",
94
- "figma-ui3-kit-svelte": "^0.6.0",
112
+ "figma-ui3-kit-svelte": "^0.8.0",
95
113
  "globals": "^17.12.0",
96
114
  "prettier": "^3.9.8",
97
115
  "prettier-plugin-svelte": "^3.4.0",
@@ -7,7 +7,7 @@
7
7
  * @example
8
8
  * <EmptyState
9
9
  * message="No items found"
10
- * icon="search"
10
+ * icon={IconSearch}
11
11
  * actions={[{ label: "Add Item", handler: handleAdd }]}
12
12
  * />
13
13
  */
@@ -15,13 +15,31 @@
15
15
  /** Message to display */
16
16
  export let message = "";
17
17
 
18
- /** Optional icon (string name or component) */
18
+ /**
19
+ * Optional icon: SVG markup, such as an icon from figma-ui3-kit-svelte/icons, or a component
20
+ * @type {string | import("svelte").ComponentType | null}
21
+ */
19
22
  export let icon = null;
20
23
 
21
- /** Single action for backward compatibility { label, handler } */
24
+ /** Icon size in px, for an icon given as SVG markup; the svg is scaled to it */
25
+ export let iconSize = 24;
26
+
27
+ /**
28
+ * @typedef {object} EmptyStateAction
29
+ * @property {string} label
30
+ * @property {() => void} handler
31
+ */
32
+
33
+ /**
34
+ * Single action for backward compatibility { label, handler }
35
+ * @type {EmptyStateAction | null}
36
+ */
22
37
  export let action = null;
23
38
 
24
- /** Multiple actions [{ label, handler }] */
39
+ /**
40
+ * Multiple actions [{ label, handler }]
41
+ * @type {EmptyStateAction[] | null}
42
+ */
25
43
  export let actions = null;
26
44
 
27
45
  /** Size variant: 'small', 'medium', 'large' */
@@ -49,7 +67,7 @@
49
67
  {#if icon}
50
68
  <div class="empty-state__icon" aria-hidden="true">
51
69
  {#if typeof icon === "string"}
52
- <Icon iconName={icon} />
70
+ <Icon iconName={icon} size={iconSize} />
53
71
  {:else}
54
72
  <svelte:component this={icon} />
55
73
  {/if}
@@ -111,6 +129,12 @@
111
129
  line-height: var(--body-large-line-height);
112
130
  }
113
131
 
132
+ /* Icon sizes its box, but the svg keeps the 24px it's drawn at. */
133
+ .empty-state__icon :global(.icon-component svg) {
134
+ width: 100%;
135
+ height: 100%;
136
+ }
137
+
114
138
  .empty-state__actions {
115
139
  display: flex;
116
140
  flex-direction: row;
@@ -1,5 +1,5 @@
1
1
  <script>
2
- import { Label } from "figma-ui3-kit-svelte";
2
+ import { Label, Text } from "figma-ui3-kit-svelte";
3
3
 
4
4
  /** Label text (optional) */
5
5
  export let label = "";
@@ -9,23 +9,58 @@
9
9
 
10
10
  /** Size of the label (optional) */
11
11
  export let size = undefined;
12
+
13
+ /**
14
+ * A line under the control: what to enter, or what the choice does
15
+ * (optional). Empty shows none, so a conditional hint is a string; the
16
+ * `hint` slot takes markup and always shows. It takes the label's size, as
17
+ * the control's error does: body-medium, or body-small in a small group.
18
+ */
19
+ export let hint = "";
12
20
  </script>
13
21
 
14
22
  <div class="field-group" class:small={size === "small"}>
15
23
  {#if label}
16
- <Label for={labelFor} {size}>{label}</Label>
24
+ <Label htmlFor={labelFor} {size}>{label}</Label>
17
25
  {/if}
18
26
  <slot />
27
+ {#if $$slots.hint || hint}
28
+ <Text
29
+ class="field-group__hint"
30
+ variant={size === "small" ? "body-small" : "body-medium"}
31
+ color="--figma-color-text-secondary"
32
+ block
33
+ >
34
+ <slot name="hint">{hint}</slot>
35
+ </Text>
36
+ {/if}
19
37
  </div>
20
38
 
21
39
  <style>
40
+ /* The control's error takes the label's size, as the hint does. Read by
41
+ the kit's Input, Textarea, Dropdown and Dropzone errors, and set on every
42
+ group so a default group inside a small one is medium again. */
22
43
  .field-group {
23
44
  display: flex;
24
45
  flex-direction: column;
25
46
  gap: var(--size-xxsmall);
47
+ --field-error-font-size: var(--body-medium-font-size);
48
+ --field-error-font-weight: var(--body-medium-font-weight);
49
+ --field-error-letter-spacing: var(--body-medium-letter-spacing);
50
+ --field-error-line-height: var(--body-medium-line-height);
26
51
  }
27
52
 
28
53
  .field-group.small {
29
54
  gap: var(--size-xxxsmall);
55
+ --field-error-font-size: var(--body-small-font-size);
56
+ --field-error-font-weight: var(--body-small-font-weight);
57
+ --field-error-letter-spacing: var(--body-small-letter-spacing);
58
+ --field-error-line-height: var(--body-small-line-height);
59
+ }
60
+
61
+ /* Like the label, the hint is interface text, not content to copy */
62
+ .field-group :global(.field-group__hint) {
63
+ cursor: default;
64
+ user-select: none;
30
65
  }
31
66
  </style>
@@ -44,12 +44,13 @@ export function formatErrorMessage(error, context) {
44
44
  // Common error patterns to make more user-friendly
45
45
  if (message.includes("Failed to fetch") || message.includes("NetworkError")) {
46
46
  userMessage =
47
- "Network error: Could not connect to the server. Please check your internet connection and try again.";
47
+ "Couldn't connect to the server. Check your internet connection and try again.";
48
48
  } else if (message.includes("CORS")) {
49
49
  userMessage =
50
- "CORS error: The resource host does not allow plugin access. Try resources from allowed domains.";
50
+ "The resource host doesn't allow plugin access. Try resources from allowed domains.";
51
51
  } else if (message.includes("JSON")) {
52
- userMessage = "Invalid JSON format. Please check your data and try again.";
52
+ userMessage =
53
+ "Couldn't read the data as JSON. Check its format and try again.";
53
54
  } else if (message.includes("not found")) {
54
55
  userMessage = message
55
56
  .replace(/not found/gi, "not found")
@@ -12,7 +12,7 @@ export function sendToUI<T extends Record<string, unknown>>(
12
12
  data?: T,
13
13
  ): void {
14
14
  if (data) {
15
- figma.ui.postMessage({ type, ...data });
15
+ figma.ui.postMessage({ ...data, type });
16
16
  } else {
17
17
  figma.ui.postMessage({ type });
18
18
  }
@@ -8,7 +8,7 @@
8
8
  * @param {object} data - Additional data to send
9
9
  */
10
10
  export function sendToPlugin(type, data = {}) {
11
- parent.postMessage({ pluginMessage: { type, ...data } }, "*");
11
+ parent.postMessage({ pluginMessage: { ...data, type } }, "*");
12
12
  }
13
13
 
14
14
  /**
package/src/lib/resize.js CHANGED
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Usage in UI:
5
5
  * import { resizeToFit, autoResize } from "figma-plugin-utilities";
6
- * resizeToFit(); // One-time resize
7
- * autoResize(); // Watch for changes and auto-resize
6
+ * resizeToFit({ container }); // One-time resize to a content wrapper
7
+ * autoResize({ container }); // Watch for changes and auto-resize
8
8
  *
9
9
  * Usage in code.ts:
10
10
  * import { handleResize } from "figma-plugin-utilities/lib/figma-helpers";
@@ -46,7 +46,10 @@ export function getContentHeight(container) {
46
46
  * @param {number} [options.minHeight=100] - Minimum height in pixels
47
47
  * @param {number} [options.maxHeight=800] - Maximum height in pixels
48
48
  * @param {number} [options.padding=0] - Extra padding to add to calculated height
49
- * @param {HTMLElement} [options.container] - Container element to measure
49
+ * @param {HTMLElement} [options.container] - Container element to measure,
50
+ * required without `height`. Not `document.body`: the kit sets
51
+ * `html, body, #app { height: 100% }`, so the body measures the window,
52
+ * which then can grow but never shrink.
50
53
  */
51
54
  export function resizeToFit(options = {}) {
52
55
  const {
@@ -55,12 +58,16 @@ export function resizeToFit(options = {}) {
55
58
  minHeight = 100,
56
59
  maxHeight = 800,
57
60
  padding = 0,
58
- container = document.body,
61
+ container,
59
62
  } = options;
60
63
 
61
64
  let finalHeight = height;
62
65
 
63
66
  if (finalHeight === undefined) {
67
+ if (!container) {
68
+ console.warn("resizeToFit: pass a height, or a container to measure");
69
+ return;
70
+ }
64
71
  finalHeight = getContentHeight(container) + padding;
65
72
  }
66
73
 
@@ -78,7 +85,7 @@ export function resizeToFit(options = {}) {
78
85
  * Use bind:this on a wrapper element that flows naturally with content.
79
86
  *
80
87
  * @param {object} options - Auto-resize options
81
- * @param {HTMLElement} options.container - Container element to observe (required, must not have fixed height)
88
+ * @param {HTMLElement | null} options.container - Container element to observe (required, must not have fixed height)
82
89
  * @param {number} [options.width] - Width in pixels (uses default if not specified)
83
90
  * @param {number} [options.minHeight=100] - Minimum height in pixels
84
91
  * @param {number} [options.maxHeight=800] - Maximum height in pixels
@@ -87,7 +94,7 @@ export function resizeToFit(options = {}) {
87
94
  * @param {number} [options.threshold=20] - Minimum height change to trigger resize (prevents position reset)
88
95
  * @returns {function} Cleanup function to stop observing
89
96
  */
90
- export function autoResize(options = {}) {
97
+ export function autoResize(options = { container: null }) {
91
98
  const {
92
99
  container,
93
100
  width = defaultWidth,
package/src/vite.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { Plugin, UserConfigFnObject } from "vite";
2
+
3
+ export function ui3InlineSvg(): Plugin;
4
+
5
+ export function inlineFigmaHtml(options: {
6
+ root: string;
7
+ moduleScript?: boolean;
8
+ }): Plugin;
9
+
10
+ export function figmaPluginConfig(
11
+ configUrl: string,
12
+ options?: { moduleScript?: boolean },
13
+ ): UserConfigFnObject;
package/src/vite.js ADDED
@@ -0,0 +1,165 @@
1
+ // Vite config for a Figma plugin: `src/index.html` and `src/code.ts` in, and
2
+ // `dist/index.html`, `dist/code.js` and `dist/manifest.json` out.
3
+ //
4
+ // Two builds, one per thread: the UI (`vite build`) and the sandbox
5
+ // (`vite build --mode code`). Built together, a module both import becomes a
6
+ // shared chunk, and code.js starts with an `import` Figma's sandbox can't load
7
+ // ("expecting '('"). Apart, each is one self-contained file, so the threads can
8
+ // share code, such as lib/scale.
9
+ import { dirname, resolve } from "node:path";
10
+ import { readFileSync } from "node:fs";
11
+ import { fileURLToPath } from "node:url";
12
+ import { svelte } from "@sveltejs/vite-plugin-svelte";
13
+
14
+ // Figma's JS sandbox doesn't support ES2020+ (no ?. or ??)
15
+ const TARGET = "es2017";
16
+
17
+ function escapeRegExp(value) {
18
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19
+ }
20
+
21
+ /**
22
+ * Bundles figma-ui3-kit-svelte's SVG imports as strings, for its icons.
23
+ * @returns {import("vite").Plugin}
24
+ */
25
+ export function ui3InlineSvg() {
26
+ return {
27
+ name: "ui3-inline-svg",
28
+ enforce: "pre",
29
+ load(id) {
30
+ if (!id.includes("figma-ui3-kit-svelte")) return;
31
+ const filePath = id.split("?")[0];
32
+ if (!filePath.endsWith(".svg")) return;
33
+ const svg = readFileSync(filePath, "utf-8");
34
+ return `export default ${JSON.stringify(svg)};`;
35
+ },
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Inlines the UI build's CSS and JS into one `index.html`, as Figma needs, and
41
+ * copies `src/manifest.json` to the output.
42
+ *
43
+ * Replacements are functions, so a `$&` in minified code isn't read as a
44
+ * pattern.
45
+ * @param {{ root: string, moduleScript?: boolean }} options
46
+ * @returns {import("vite").Plugin}
47
+ */
48
+ export function inlineFigmaHtml({ root, moduleScript = false }) {
49
+ const scriptTag = moduleScript ? '<script type="module">' : "<script>";
50
+ return {
51
+ name: "inline-figma-html",
52
+ apply: "build",
53
+ enforce: "post",
54
+ generateBundle(_options, bundle) {
55
+ const htmlKey =
56
+ Object.keys(bundle).find((key) => key.endsWith("index.html")) ?? "";
57
+ const htmlAsset = htmlKey ? bundle[htmlKey] : undefined;
58
+ if (!htmlAsset || htmlAsset.type !== "asset") return;
59
+
60
+ let html = String(htmlAsset.source);
61
+ html = html.replace(/<link\s+[^>]*rel=["']modulepreload["'][^>]*>/gi, "");
62
+
63
+ for (const [fileName, asset] of Object.entries(bundle)) {
64
+ if (asset.type !== "asset" || !fileName.endsWith(".css")) continue;
65
+ const css = String(asset.source ?? "");
66
+ const hrefPattern = new RegExp(
67
+ `<link[^>]+rel=["']stylesheet["'][^>]+href=["'](?:\\./|/|\\.\\./)?${escapeRegExp(
68
+ fileName,
69
+ )}["'][^>]*>`,
70
+ "i",
71
+ );
72
+ if (hrefPattern.test(html)) {
73
+ html = html.replace(hrefPattern, () => `<style>${css}</style>`);
74
+ delete bundle[fileName];
75
+ }
76
+ }
77
+
78
+ for (const [fileName, chunk] of Object.entries(bundle)) {
79
+ if (chunk.type !== "chunk") continue;
80
+ const scriptPattern = new RegExp(
81
+ `<script[^>]+src=["'](?:\\./|/|\\.\\./)?${escapeRegExp(
82
+ fileName,
83
+ )}["'][^>]*></script>`,
84
+ "i",
85
+ );
86
+ if (scriptPattern.test(html)) {
87
+ html = html.replace(
88
+ scriptPattern,
89
+ () => `${scriptTag}${chunk.code}</script>`,
90
+ );
91
+ delete bundle[fileName];
92
+ }
93
+ }
94
+
95
+ htmlAsset.source = html;
96
+ if (htmlKey !== "index.html") {
97
+ delete bundle[htmlKey];
98
+ htmlAsset.fileName = "index.html";
99
+ bundle["index.html"] = htmlAsset;
100
+ }
101
+
102
+ this.emitFile({
103
+ type: "asset",
104
+ fileName: "manifest.json",
105
+ source: readFileSync(resolve(root, "src/manifest.json"), "utf-8"),
106
+ });
107
+ },
108
+ };
109
+ }
110
+
111
+ /**
112
+ * The plugin's Vite config, for `vite.config.ts`:
113
+ *
114
+ * export default figmaPluginConfig(import.meta.url);
115
+ *
116
+ * Build with `vite build && vite build --mode code`.
117
+ *
118
+ * `moduleScript` inlines the UI as `<script type="module">`, which runs once
119
+ * the page is parsed. Without it the script is classic and runs in `<head>`,
120
+ * so `main.js` waits for DOMContentLoaded before it mounts.
121
+ * @param {string} configUrl `import.meta.url` of the plugin's vite.config.ts
122
+ * @param {{ moduleScript?: boolean }} [options]
123
+ * @returns {import("vite").UserConfigFnObject}
124
+ */
125
+ export function figmaPluginConfig(configUrl, { moduleScript = false } = {}) {
126
+ const root = dirname(fileURLToPath(configUrl));
127
+ return ({ mode }) =>
128
+ mode === "code"
129
+ ? {
130
+ base: "./",
131
+ build: {
132
+ outDir: "dist",
133
+ emptyOutDir: false, // The UI build's index.html and manifest.json stay
134
+ target: TARGET,
135
+ rollupOptions: {
136
+ input: resolve(root, "src/code.ts"),
137
+ output: {
138
+ entryFileNames: "code.js",
139
+ inlineDynamicImports: true,
140
+ },
141
+ },
142
+ },
143
+ }
144
+ : {
145
+ base: "./",
146
+ plugins: [
147
+ ui3InlineSvg(),
148
+ svelte(),
149
+ inlineFigmaHtml({ root, moduleScript }),
150
+ ],
151
+ build: {
152
+ outDir: "dist",
153
+ emptyOutDir: false, // The sandbox build's code.js stays
154
+ target: TARGET,
155
+ rollupOptions: {
156
+ input: resolve(root, "src/index.html"),
157
+ output: {
158
+ entryFileNames: "[name].js",
159
+ chunkFileNames: "[name]-[hash].js",
160
+ assetFileNames: "[name]-[hash][extname]",
161
+ },
162
+ },
163
+ },
164
+ };
165
+ }