figma-rust-mcp 3.2.7 → 3.2.8
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 +40 -0
- package/bin/figma-mcp.js +7 -4
- package/package.json +3 -2
- package/plugin/code.js +1 -1
- package/scripts/build-plugin.js +21 -4
- package/scripts/test-plugin-tokens.mjs +147 -0
package/README.md
CHANGED
|
@@ -107,6 +107,25 @@ npx -y figma-rust-mcp@latest --uninstall-service
|
|
|
107
107
|
|
|
108
108
|
---
|
|
109
109
|
|
|
110
|
+
### Windows background mode
|
|
111
|
+
|
|
112
|
+
Double-clicking `figma-mcp.exe` starts the server in a detached process and closes
|
|
113
|
+
its launcher console. From PowerShell or Command Prompt, use:
|
|
114
|
+
|
|
115
|
+
```powershell
|
|
116
|
+
.\figma-mcp.exe --background
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The server keeps running after the terminal closes. Logs are appended to
|
|
120
|
+
`%LOCALAPPDATA%\figma-mcp\logs\server.log`. Use `--server` for the foreground
|
|
121
|
+
terminal dashboard, or `--stdio` for an MCP client subprocess.
|
|
122
|
+
|
|
123
|
+
`--install-service` through the npm runner configures background startup at login.
|
|
124
|
+
Re-run the installation command to update an existing Windows task. Removing the
|
|
125
|
+
task disables future login startup; stop an already detached server through Task
|
|
126
|
+
Manager (`figma-mcp.exe`). The launcher console may appear briefly when opening
|
|
127
|
+
the executable; the server itself does not retain a console window.
|
|
128
|
+
|
|
110
129
|
### 4. Build from Source (Optional)
|
|
111
130
|
|
|
112
131
|
If you prefer building directly with the [Rust toolchain](https://rustup.rs/) (`cargo >= 1.80`):
|
|
@@ -215,6 +234,27 @@ Exports Figma Variables (Design Tokens), Color Styles, Typography Styles, and El
|
|
|
215
234
|
- **`outputPath`**: Writes directly to disk (e.g. `src/styles/tokens.css` or `tailwind.config.js`).
|
|
216
235
|
- **`collection`** / **`mode`** / **`prefix`**: Optional filters and naming prefixes.
|
|
217
236
|
|
|
237
|
+
Token exports use schema v2 and preserve full paint/effect stacks, alpha, gradient
|
|
238
|
+
transforms, collection modes and raw alias values. CSS, Tailwind, TypeScript and
|
|
239
|
+
JSON share one normalized model. The `w3c` output uses DTCG 2025.10 color,
|
|
240
|
+
dimension, gradient-stop and shadow values, with Figma-specific geometry and
|
|
241
|
+
unsupported values retained in `$extensions`.
|
|
242
|
+
|
|
243
|
+
Linear gradients on a node are projected using its actual dimensions. A diagonal
|
|
244
|
+
gradient style has no target aspect ratio: use `get_css` on its consuming node.
|
|
245
|
+
Radial/angular/diamond paints, unsupported blend modes and progressive blurs
|
|
246
|
+
retain their raw data and report diagnostics recommending SVG instead of emitting
|
|
247
|
+
approximate CSS. Unknown modes, unresolved aliases and sanitized name collisions
|
|
248
|
+
fail explicitly. CSS exports include non-default modes under `data-theme`
|
|
249
|
+
selectors; opacity/font-weight variables stay unitless.
|
|
250
|
+
|
|
251
|
+
The plugin UI badge is stamped from the build version and updated from the
|
|
252
|
+
connected server handshake. Package/Cargo/tag mismatches fail the build pipeline.
|
|
253
|
+
Run `npm run build:plugin` after changing plugin source, and `npm run test:tokens`
|
|
254
|
+
for the token/contract/version regression suite. Existing background servers need
|
|
255
|
+
a restart with the new binary to load the updated runtime.
|
|
256
|
+
|
|
257
|
+
|
|
218
258
|
### 5. `figma_get_selection`
|
|
219
259
|
Token-compressed inspection of currently selected layers/frames on the Figma canvas. Returns compacted layout and typography hierarchy with **60–80% fewer tokens**.
|
|
220
260
|
- **`detail`**: `"compact"` (recommended), `"minimal"`, `"full"`.
|
package/bin/figma-mcp.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* caches it locally, and executes it transparently.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { spawn, execSync } from 'node:child_process';
|
|
9
|
+
import { spawn, execSync, execFileSync } from 'node:child_process';
|
|
10
10
|
import fs from 'node:fs';
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import os from 'node:os';
|
|
@@ -19,7 +19,7 @@ const __dirname = path.dirname(__filename);
|
|
|
19
19
|
|
|
20
20
|
// Read package.json version
|
|
21
21
|
const pkgPath = path.resolve(__dirname, '../package.json');
|
|
22
|
-
let pkgVersion =
|
|
22
|
+
let pkgVersion = null;
|
|
23
23
|
let repoOwner = 'BuiHung1612';
|
|
24
24
|
let repoName = 'figma-mcp';
|
|
25
25
|
|
|
@@ -36,6 +36,7 @@ try {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
} catch (_) {}
|
|
39
|
+
if (!pkgVersion) throw new Error(`Cannot read package version from ${pkgPath}`);
|
|
39
40
|
|
|
40
41
|
function getPlatformArch() {
|
|
41
42
|
const platform = process.platform;
|
|
@@ -293,8 +294,10 @@ WantedBy=default.target
|
|
|
293
294
|
} else if (platform === 'win32') {
|
|
294
295
|
const taskName = 'FigmaMCPServer';
|
|
295
296
|
// Use schtasks to create a task that runs at login
|
|
296
|
-
|
|
297
|
-
|
|
297
|
+
// The native launcher detaches the server and redirects output to a log file.
|
|
298
|
+
const taskCommand = `"${binPath}" --background`;
|
|
299
|
+
execFileSync('schtasks', ['/Create', '/F', '/TN', taskName,
|
|
300
|
+
'/TR', taskCommand, '/SC', 'ONLOGON'], { stdio: 'inherit', windowsHide: true });
|
|
298
301
|
|
|
299
302
|
// Start it immediately too
|
|
300
303
|
try { execSync(`schtasks /Run /TN "${taskName}"`, { stdio: 'pipe' }); } catch (_) {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figma-rust-mcp",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.8",
|
|
4
4
|
"description": "Claude Code to Figma — Bidirectional MCP bridge. AI draws UI on Figma canvas, reads designs back. Works with Claude, Cursor, VS Code, Windsurf, Antigravity. No API key needed.",
|
|
5
5
|
"mcpName": "io.github.BuiHung1612/figma-mcp",
|
|
6
6
|
"type": "module",
|
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
"LICENSE"
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
|
-
"build": "cargo build --release",
|
|
19
|
+
"build": "npm run build:plugin && cargo build --release",
|
|
20
20
|
"build:plugin": "node scripts/build-plugin.js",
|
|
21
21
|
"test": "./scripts/test-rust-mcp.sh",
|
|
22
|
+
"test:tokens": "node --test scripts/test-plugin-tokens.mjs && cargo test",
|
|
22
23
|
"dev": "cargo run"
|
|
23
24
|
},
|
|
24
25
|
"engines": {
|
package/plugin/code.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// ─── FIGMA MCP BRIDGE — DYNAMIC THIN LOADER
|
|
1
|
+
// ─── FIGMA MCP BRIDGE — DYNAMIC THIN LOADER ───────────────────────────
|
|
2
2
|
// This file is a permanent micro-stub. You never need to reinstall or edit this file.
|
|
3
3
|
// The actual runtime is loaded dynamically from the local figma-mcp Rust engine.
|
|
4
4
|
|
package/scripts/build-plugin.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
// Build script — concatenate src
|
|
1
|
+
// Build script — concatenate plugin-src/*.js into plugin-runtime/code.js
|
|
2
2
|
// No module system used: files are plain JS sharing a single scope when concatenated.
|
|
3
3
|
import { readFileSync, writeFileSync } from "fs";
|
|
4
4
|
|
|
5
5
|
const { version } = JSON.parse(readFileSync("package.json", "utf8"));
|
|
6
|
+
const cargoVersion = readFileSync("Cargo.toml", "utf8").match(/^version\s*=\s*"([^"]+)"/m)?.[1];
|
|
7
|
+
if (version !== cargoVersion) throw new Error(`Version mismatch: package.json ${version}, Cargo.toml ${cargoVersion}`);
|
|
8
|
+
if (process.env.GITHUB_REF_TYPE === "tag" && process.env.GITHUB_REF_NAME !== `v${version}`) {
|
|
9
|
+
throw new Error(`Release tag ${process.env.GITHUB_REF_NAME} does not match build version v${version}`);
|
|
10
|
+
}
|
|
6
11
|
|
|
7
12
|
const files = [
|
|
8
13
|
"plugin-src/utils.js",
|
|
9
14
|
"plugin-src/svg-path-helpers.js",
|
|
10
15
|
"plugin-src/paint-and-effects.js",
|
|
16
|
+
"plugin-src/token-helpers.js",
|
|
11
17
|
"plugin-src/read-helpers.js",
|
|
12
18
|
"plugin-src/handlers-write.js",
|
|
13
19
|
"plugin-src/handlers-library.js",
|
|
@@ -18,10 +24,9 @@ const files = [
|
|
|
18
24
|
"plugin-src/main.js",
|
|
19
25
|
];
|
|
20
26
|
|
|
21
|
-
const banner = `// figma-mcp — Figma Plugin main thread
|
|
27
|
+
const banner = `// figma-mcp — Figma Plugin main thread v${version}
|
|
22
28
|
// Handles both WRITE (draw UI) and READ (extract design) operations.
|
|
23
29
|
// AUTO-GENERATED by scripts/build-plugin.js — edit plugin-src/*.js instead
|
|
24
|
-
// Generated: ${new Date().toISOString()}
|
|
25
30
|
`;
|
|
26
31
|
|
|
27
32
|
const parts = files.map(f => {
|
|
@@ -30,5 +35,17 @@ const parts = files.map(f => {
|
|
|
30
35
|
});
|
|
31
36
|
|
|
32
37
|
const output = (banner + parts.join("\n")).replace(/\{\{PLUGIN_VERSION\}\}/g, version);
|
|
33
|
-
|
|
38
|
+
|
|
39
|
+
const uiPath = "plugin-runtime/ui.html";
|
|
40
|
+
const ui = readFileSync(uiPath, "utf8").replace(/(<span class="brand-ver"[^>]*>)[^<]*(<\/span>)/,
|
|
41
|
+
`$1v${version}$2`);
|
|
42
|
+
if (!ui.includes(`id="runtime-version">v${version}</span>`)) throw new Error("Missing runtime version badge");
|
|
43
|
+
if (process.argv.includes('--check')) {
|
|
44
|
+
if (readFileSync("plugin-runtime/code.js", "utf8") !== output || readFileSync(uiPath, "utf8") !== ui) {
|
|
45
|
+
throw new Error("Plugin bundle is stale; run npm run build:plugin");
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
writeFileSync("plugin-runtime/code.js", output);
|
|
49
|
+
writeFileSync(uiPath, ui);
|
|
50
|
+
}
|
|
34
51
|
console.log(`✓ plugin-runtime/code.js written (${output.length} chars, ${output.split("\n").length} lines)`);
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import vm from 'node:vm';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
function runtime(figma = {}) {
|
|
8
|
+
const context = vm.createContext({ figma, handlers: {}, console, setTimeout, Map, Set, Uint8Array });
|
|
9
|
+
for (const file of ['utils', 'token-helpers', 'handlers-read-detail', 'handlers-read', 'handlers-tokens', 'handlers-write-ops']) {
|
|
10
|
+
vm.runInContext(readFileSync(`plugin-src/${file}.js`, 'utf8'), context, { filename: file });
|
|
11
|
+
}
|
|
12
|
+
return context;
|
|
13
|
+
}
|
|
14
|
+
const plain = value => JSON.parse(JSON.stringify(value));
|
|
15
|
+
const gradient = (transform = [[1, 0, 0], [0, 1, 0]]) => ({
|
|
16
|
+
type: 'GRADIENT_LINEAR', gradientTransform: transform, opacity: .5,
|
|
17
|
+
gradientStops: [{ position: 0, color: { r: 1, g: 0, b: 0, a: .5 } }, { position: 1, color: { r: 0, g: 0, b: 1, a: 1 } }],
|
|
18
|
+
});
|
|
19
|
+
const shadow = type => ({ type, color: { r: 0, g: 0, b: 0, a: .125 }, offset: { x: -1, y: 3 }, radius: 6, spread: -2, visible: true });
|
|
20
|
+
|
|
21
|
+
test('equivalent CSS syntaxes preserve alpha in reads and writes', () => {
|
|
22
|
+
const r = runtime();
|
|
23
|
+
for (const color of ['rgba(255, 0, 0, .5)', 'rgb(100% 0% 0% / 50%)', 'hsl(0 100% 50% / .5)', 'hsla(0, 100%, 50%, .5)']) {
|
|
24
|
+
assert.deepEqual(plain(r.parseColorValue(color)), { r: 1, g: 0, b: 0, a: .5 });
|
|
25
|
+
assert.equal(r.hexToRgbA(color).a, .5);
|
|
26
|
+
assert.equal(r.solidFill(color)[0].opacity, .5);
|
|
27
|
+
}
|
|
28
|
+
assert.equal(r.parseColorValue('#f008').a, 136 / 255);
|
|
29
|
+
assert.equal(r.parseColorValue('transparent').a, 0);
|
|
30
|
+
assert.equal(r.colorToCss({ r: 0, g: 0, b: 0, a: .9999 }), 'rgba(0, 0, 0, 0.9999)');
|
|
31
|
+
assert.throws(() => r.parseColorValue('rgba(garbage)'), /Invalid/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('gradient projection includes translations, aspect ratio, stop and paint alpha', () => {
|
|
35
|
+
const r = runtime();
|
|
36
|
+
const p = r.serializePaint(gradient());
|
|
37
|
+
assert.deepEqual(plain(p.gradientTransform), [[1, 0, 0], [0, 1, 0]]);
|
|
38
|
+
assert.match(r.paintToCss(p, 200, 100), /^linear-gradient\(90deg, rgba\(255, 0, 0, 0.25\) 0%, rgba\(0, 0, 255, 0.5\) 100%\)$/);
|
|
39
|
+
const diagonal = r.paintToCss(r.serializePaint(gradient([[1, 1, 0], [0, 1, 0]])), 200, 100);
|
|
40
|
+
assert.match(diagonal, /153\.434948/);
|
|
41
|
+
assert.match(diagonal, /50%/);
|
|
42
|
+
const shifted = r.paintToCss(r.serializePaint(gradient([[.5, 0, .25], [0, 1, 0]])), 200, 100);
|
|
43
|
+
assert.match(shifted, /-50%/);
|
|
44
|
+
assert.match(shifted, /150%/);
|
|
45
|
+
assert.throws(() => r.paintToCss({ ...p, type: 'GRADIENT_DIAMOND' }, 100, 100), /SVG/);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('get_css uses the numeric paint, never hex slicing a serialized rgba', async () => {
|
|
49
|
+
const node = { id: '1:2', name: 'alpha', type: 'RECTANGLE', x: 0, y: 0, width: 200, height: 100,
|
|
50
|
+
fills: [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 }, opacity: .25 }], effects: [shadow('DROP_SHADOW'), shadow('INNER_SHADOW')] };
|
|
51
|
+
const r = runtime();
|
|
52
|
+
r.findNodeByIdAsync = async () => node;
|
|
53
|
+
const result = await r.handlers.get_css({ id: node.id });
|
|
54
|
+
assert.match(result.css, /background-color: rgba\(255, 0, 0, 0.25\);/);
|
|
55
|
+
assert.doesNotMatch(result.css, /NaN|0.0625/);
|
|
56
|
+
assert.match(result.css, /inset -1px 3px 6px -2px rgba\(0, 0, 0, 0.125\)/);
|
|
57
|
+
assert.equal(result.detail.effects.length, 2);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('styles retain full paints, effects, units and disabled layers', async () => {
|
|
61
|
+
const r = runtime({
|
|
62
|
+
getLocalPaintStylesAsync: async () => [{ id: 'p', name: 'Gradient', paints: [gradient()] }],
|
|
63
|
+
getLocalTextStylesAsync: async () => [{ id: 't', name: 'Body', fontName: { family: 'Inter', style: 'Regular' }, fontSize: 16, lineHeight: { unit: 'PERCENT', value: 150 }, letterSpacing: { unit: 'PERCENT', value: 2 } }],
|
|
64
|
+
getLocalEffectStylesAsync: async () => [{ id: 's', name: 'Card', effects: [shadow('DROP_SHADOW'), shadow('INNER_SHADOW'), { type: 'LAYER_BLUR', radius: 8, visible: false }] }],
|
|
65
|
+
getLocalGridStylesAsync: async () => [],
|
|
66
|
+
});
|
|
67
|
+
const styles = await r.handlers.get_styles();
|
|
68
|
+
assert.equal(styles.schemaVersion, 2);
|
|
69
|
+
assert.equal(styles.paintStyles[0].paints[0].gradientStops[0].rgba.a, .5);
|
|
70
|
+
assert.equal(styles.effectStyles[0].effects.length, 3);
|
|
71
|
+
assert.equal(styles.textStyles[0].lineHeightUnit, 'PERCENT');
|
|
72
|
+
assert.equal(r.effectsToCss(styles.effectStyles[0].effects).filter, '');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('aliases use mode names across collections and retain false/zero values', async () => {
|
|
76
|
+
const cols = [
|
|
77
|
+
{ id: 'semantic', name: 'Semantic', defaultModeId: 's-light', modes: [{ modeId: 's-light', name: 'Light' }, { modeId: 's-dark', name: 'Dark' }], variableIds: ['alias', 'zero'] },
|
|
78
|
+
{ id: 'primitive', name: 'Primitive', defaultModeId: 'p-light', modes: [{ modeId: 'p-light', name: 'Light' }, { modeId: 'p-dark', name: 'Dark' }], variableIds: ['base'] },
|
|
79
|
+
];
|
|
80
|
+
const variables = [
|
|
81
|
+
{ id: 'alias', name: 'semantic', variableCollectionId: 'semantic', resolvedType: 'BOOLEAN', valuesByMode: { 's-light': { type: 'VARIABLE_ALIAS', id: 'base' }, 's-dark': { type: 'VARIABLE_ALIAS', id: 'base' } } },
|
|
82
|
+
{ id: 'zero', name: 'zero', variableCollectionId: 'semantic', resolvedType: 'FLOAT', valuesByMode: { 's-light': 0, 's-dark': 1 } },
|
|
83
|
+
{ id: 'base', name: 'base', variableCollectionId: 'primitive', resolvedType: 'BOOLEAN', valuesByMode: { 'p-light': false, 'p-dark': true } },
|
|
84
|
+
];
|
|
85
|
+
const r = runtime({ variables: {
|
|
86
|
+
getLocalVariableCollectionsAsync: async () => cols, getLocalVariablesAsync: async () => variables,
|
|
87
|
+
getVariableByIdAsync: async id => variables.find(v => v.id === id),
|
|
88
|
+
getVariableCollectionByIdAsync: async id => cols.find(c => c.id === id),
|
|
89
|
+
} });
|
|
90
|
+
const result = await r.handlers.get_variables();
|
|
91
|
+
const alias = result.collections[0].variables[0];
|
|
92
|
+
assert.equal(alias.values['s-light'].resolvedValue, false);
|
|
93
|
+
assert.equal(alias.values['s-dark'].resolvedValue, true);
|
|
94
|
+
assert.equal(result.resolvedTokens.semantic, false);
|
|
95
|
+
assert.equal(result.resolvedTokens.zero, 0);
|
|
96
|
+
assert.equal(result.collections[0].defaultModeId, 's-light');
|
|
97
|
+
assert.deepEqual(plain(result.diagnostics), []);
|
|
98
|
+
const consumer = { resolvedVariableModes: { semantic: 's-dark', primitive: 'p-light' } };
|
|
99
|
+
variables[0].resolveForConsumer = () => ({ value: false, resolvedType: 'BOOLEAN' });
|
|
100
|
+
assert.equal((await r.resolveVariableValueAsync(variables[0], consumer)).resolvedValue, false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('export_node validates format and works through batch and aliases', async () => {
|
|
104
|
+
const r = runtime();
|
|
105
|
+
r.handlers.export_svg = async () => ({ svg: '<svg/>' });
|
|
106
|
+
r.handlers.export_image = async params => ({ format: params.format });
|
|
107
|
+
assert.equal((await r.resolveOperationHandler('exportNode')({ format: 'svg' })).svg, '<svg/>');
|
|
108
|
+
assert.equal((await r.handlers.export_node({ format: 'jpeg' })).format, 'JPG');
|
|
109
|
+
const batch = await r.handlers.batch({ operations: [{ operation: 'export_node', params: { format: 'SVG' } }, { operation: 'export_node', params: { format: 'bad' } }] });
|
|
110
|
+
assert.equal(batch.succeeded, 1);
|
|
111
|
+
assert.match(batch.results[1].error, /supports PNG/);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('multiple paints preserve top-first ordering and node opacity stays separate', () => {
|
|
115
|
+
const r = runtime();
|
|
116
|
+
const paints = [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 }, opacity: .5 }, { type: 'SOLID', color: { r: 0, g: 0, b: 1 }, opacity: 1 }].map(r.serializePaint);
|
|
117
|
+
const css = r.paintsToCss(paints, 200, 100);
|
|
118
|
+
assert.match(css.value, /^linear-gradient\(rgba\(255, 0, 0, 0.5\)/);
|
|
119
|
+
assert.match(css.value, /linear-gradient\(#0000ff, #0000ff\)$/);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('UI badge uses release version initially and follows the server on reconnect', () => {
|
|
123
|
+
const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
|
|
124
|
+
const html = readFileSync('plugin-runtime/ui.html', 'utf8');
|
|
125
|
+
assert.ok(html.includes(`id="runtime-version">v${pkg.version}</span>`));
|
|
126
|
+
const start = html.indexOf(' function updateRuntimeVersion(');
|
|
127
|
+
const end = html.indexOf(' var runtimeCapabilities', start);
|
|
128
|
+
const badge = {};
|
|
129
|
+
const context = vm.createContext({ document: { getElementById: () => badge } });
|
|
130
|
+
vm.runInContext(html.slice(start, end), context);
|
|
131
|
+
context.updateRuntimeVersion('3.2.7');
|
|
132
|
+
assert.equal(badge.textContent, 'v3.2.7');
|
|
133
|
+
context.updateRuntimeVersion('3.3.0');
|
|
134
|
+
assert.equal(badge.textContent, 'v3.3.0');
|
|
135
|
+
assert.ok(html.includes('updateRuntimeVersion(serverVer)'));
|
|
136
|
+
assert.ok(html.includes('type: "runtime-ready"'));
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('build rejects wrong release tags and checked-in bundle matches source', () => {
|
|
140
|
+
const check = spawnSync(process.execPath, ['scripts/build-plugin.js', '--check'], { encoding: 'utf8' });
|
|
141
|
+
assert.equal(check.status, 0, check.stderr);
|
|
142
|
+
const rejected = spawnSync(process.execPath, ['scripts/build-plugin.js'], {
|
|
143
|
+
encoding: 'utf8', env: { ...process.env, GITHUB_REF_TYPE: 'tag', GITHUB_REF_NAME: 'v0.0.0-invalid' },
|
|
144
|
+
});
|
|
145
|
+
assert.notEqual(rejected.status, 0);
|
|
146
|
+
assert.match(rejected.stderr, /does not match build version/);
|
|
147
|
+
});
|