winden-tokens 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Marko Krstić (mailme@markokrstic.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/cli.mjs ADDED
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `winden-tokens` — the executable published to npm.
4
+ *
5
+ * Starts the bridge relay (see ./server.mjs) and serves the prebuilt browser UI
6
+ * from the same origin, then opens a browser at it. A user needs this package
7
+ * and Node; they need neither the plugin's source tree nor vite.
8
+ *
9
+ * The UI is resolved in this order:
10
+ * 1. ./ui/index.html — shipped in the published tarball (see scripts/build-ui.mjs)
11
+ * 2. ../dist/index.html — this repo's own build, so a checkout works without packing
12
+ * 3. nothing — the relay still runs; `/` explains what is missing
13
+ *
14
+ * `--dev` flips 1 and 2. A maintainer who has run `npm pack` has a `ui/` in
15
+ * their checkout, and it would otherwise shadow the build they just made —
16
+ * serving yesterday's UI while they debug today's.
17
+ */
18
+
19
+ import { spawn } from 'node:child_process';
20
+ import { existsSync } from 'node:fs';
21
+ import { readFile } from 'node:fs/promises';
22
+ import { dirname, join } from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
24
+
25
+ import { DEFAULT_PORT, HOST, PROTOCOL_VERSION, startRelay } from './server.mjs';
26
+
27
+ const HERE = dirname(fileURLToPath(import.meta.url));
28
+
29
+ const HELP = `
30
+ winden-tokens — run the Winden Tokens UI in a browser, driving the Figma file
31
+ that is currently open.
32
+
33
+ Usage
34
+ winden-tokens [options]
35
+
36
+ Options
37
+ --port <n> Port for the relay and the UI (default ${DEFAULT_PORT}).
38
+ --no-open Do not open a browser.
39
+ --dev Also accept a client from the vite dev server
40
+ (http://localhost:5173). Only for developing the UI itself.
41
+ -h, --help This text.
42
+ -v, --version Print the version.
43
+
44
+ Environment
45
+ WINDEN_BRIDGE_PORT same as --port
46
+ WINDEN_BRIDGE_DEV=1 same as --dev
47
+
48
+ How to use it
49
+ 1. Run this command. It prints a URL and opens it.
50
+ 2. Open the Winden Tokens plugin in Figma, on the file you want to work on.
51
+ The plugin window must stay open: it is the only thing that can talk to
52
+ the Figma API. It collapses to a status strip while the tab has the wheel.
53
+ 3. The browser tab now shows that file, full size.
54
+
55
+ About --port
56
+ The port is part of the contract, not a free choice. The Figma plugin can
57
+ only open a socket to a host its manifest lists, and the manifest lists
58
+ ws://localhost:${DEFAULT_PORT}. Moving the relay with --port also needs the plugin's
59
+ manifest.json to list the new port, or the plugin will never connect.
60
+ Use it to dodge a port conflict only if you can rebuild the plugin too.
61
+
62
+ Security
63
+ The relay binds 127.0.0.1 only, and refuses any WebSocket whose Origin is
64
+ not the page it served itself. A WebSocket handshake is not subject to
65
+ CORS, so that check is the only thing between a hostile page and your
66
+ Figma file. Do not widen it.
67
+ `;
68
+
69
+ function parseArgs(argv) {
70
+ const opts = {
71
+ port: null,
72
+ open: true,
73
+ dev: process.env.WINDEN_BRIDGE_DEV === '1' || process.env.WINDEN_BRIDGE_DEV === 'true',
74
+ help: false,
75
+ version: false,
76
+ };
77
+
78
+ for (let i = 0; i < argv.length; i++) {
79
+ const arg = argv[i];
80
+
81
+ if (arg === '--help' || arg === '-h') {
82
+ opts.help = true;
83
+ } else if (arg === '--version' || arg === '-v') {
84
+ opts.version = true;
85
+ } else if (arg === '--no-open') {
86
+ opts.open = false;
87
+ } else if (arg === '--open') {
88
+ opts.open = true;
89
+ } else if (arg === '--dev') {
90
+ opts.dev = true;
91
+ } else if (arg === '--port') {
92
+ opts.port = argv[++i];
93
+ } else if (arg.startsWith('--port=')) {
94
+ opts.port = arg.slice('--port='.length);
95
+ } else {
96
+ fail(`Unknown option: ${arg}\nRun \`winden-tokens --help\`.`);
97
+ }
98
+ }
99
+
100
+ return opts;
101
+ }
102
+
103
+ function fail(message) {
104
+ console.error(`\n ${message}\n`);
105
+ process.exit(1);
106
+ }
107
+
108
+ /** A port is either a valid integer in range or a hard error — never a fallback. */
109
+ function resolvePort(fromFlag) {
110
+ const raw = fromFlag ?? process.env.WINDEN_BRIDGE_PORT;
111
+ if (raw === undefined || raw === null || raw === '') return DEFAULT_PORT;
112
+
113
+ const n = Number(raw);
114
+ if (!Number.isInteger(n) || n < 1 || n > 65535) {
115
+ fail(`"${raw}" is not a valid port (1-65535).`);
116
+ }
117
+ return n;
118
+ }
119
+
120
+ function resolveUiFile(dev) {
121
+ const packaged = join(HERE, 'ui', 'index.html');
122
+ const checkout = join(HERE, '..', 'dist', 'index.html');
123
+
124
+ for (const candidate of dev ? [checkout, packaged] : [packaged, checkout]) {
125
+ if (existsSync(candidate)) return candidate;
126
+ }
127
+ return null;
128
+ }
129
+
130
+ /**
131
+ * Open the platform's default browser.
132
+ *
133
+ * `child_process` and the platform's own opener on purpose — this package has
134
+ * exactly one runtime dependency (`ws`) and is not spending a second one on
135
+ * three lines of argv. No shell: the URL is built from a validated integer
136
+ * port, and passing it as an argv entry keeps it that way.
137
+ */
138
+ function openBrowser(url) {
139
+ const [command, args] =
140
+ process.platform === 'darwin'
141
+ ? ['open', [url]]
142
+ : process.platform === 'win32'
143
+ ? ['cmd', ['/c', 'start', '', url]]
144
+ : ['xdg-open', [url]];
145
+
146
+ try {
147
+ const child = spawn(command, args, { detached: true, stdio: 'ignore' });
148
+ child.on('error', () => {
149
+ console.log(` (could not launch a browser — open ${url} yourself)`);
150
+ });
151
+ child.unref();
152
+ } catch {
153
+ console.log(` (could not launch a browser — open ${url} yourself)`);
154
+ }
155
+ }
156
+
157
+ async function version() {
158
+ try {
159
+ const pkg = JSON.parse(await readFile(join(HERE, 'package.json'), 'utf8'));
160
+ return pkg.version ?? 'unknown';
161
+ } catch {
162
+ return 'unknown';
163
+ }
164
+ }
165
+
166
+ async function main() {
167
+ const opts = parseArgs(process.argv.slice(2));
168
+
169
+ if (opts.help) {
170
+ console.log(HELP);
171
+ return;
172
+ }
173
+
174
+ if (opts.version) {
175
+ console.log(await version());
176
+ return;
177
+ }
178
+
179
+ const port = resolvePort(opts.port);
180
+ const uiFile = resolveUiFile(opts.dev);
181
+
182
+ let relay;
183
+ try {
184
+ relay = await startRelay({ port, dev: opts.dev, uiFile });
185
+ } catch (err) {
186
+ if (err.code === 'EADDRINUSE') {
187
+ fail(
188
+ `Cannot start: ${HOST}:${port} is already in use.\n` +
189
+ ` Another winden-tokens is probably already running — try opening\n` +
190
+ ` http://${HOST}:${port}/ instead. Otherwise stop it, or pass --port\n` +
191
+ ` (and see \`winden-tokens --help\` about what --port does NOT change).`
192
+ );
193
+ }
194
+ fail(`Cannot start: ${err.message}`);
195
+ return;
196
+ }
197
+
198
+ console.log(`\n Winden Tokens bridge — protocol v${PROTOCOL_VERSION}`);
199
+ console.log(` UI: ${relay.url}`);
200
+ console.log(` Relay: ws://${HOST}:${port}`);
201
+ if (!uiFile) {
202
+ console.log('\n NOTE: no browser UI is bundled with this relay, so the URL above is empty.');
203
+ }
204
+ console.log('\n Now open the Winden Tokens plugin in Figma and leave that window open.');
205
+ console.log(' Ctrl-C to stop.\n');
206
+
207
+ if (opts.open && uiFile) openBrowser(relay.url);
208
+
209
+ for (const sig of ['SIGINT', 'SIGTERM']) {
210
+ process.on(sig, () => {
211
+ relay.close().then(() => process.exit(0));
212
+ setTimeout(() => process.exit(0), 500).unref();
213
+ });
214
+ }
215
+ }
216
+
217
+ main().catch((err) => {
218
+ fail(err?.stack ?? String(err));
219
+ });
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "winden-tokens",
3
+ "version": "0.1.0",
4
+ "description": "Run the Winden Tokens Figma plugin UI full-size in a browser, driving the Figma file you have open. Local relay + prebuilt UI, loopback only.",
5
+ "type": "module",
6
+ "bin": {
7
+ "winden-tokens": "cli.mjs"
8
+ },
9
+ "main": "server.mjs",
10
+ "exports": {
11
+ ".": "./server.mjs",
12
+ "./package.json": "./package.json"
13
+ },
14
+ "files": [
15
+ "cli.mjs",
16
+ "server.mjs",
17
+ "ui/index.html"
18
+ ],
19
+ "engines": {
20
+ "node": ">=18.0.0"
21
+ },
22
+ "scripts": {
23
+ "prepack": "node ./scripts/build-ui.mjs"
24
+ },
25
+ "dependencies": {
26
+ "ws": "^8.21.3"
27
+ },
28
+ "keywords": [
29
+ "figma",
30
+ "figma-plugin",
31
+ "design-tokens",
32
+ "winden",
33
+ "bridge"
34
+ ],
35
+ "author": "Marko Krstic",
36
+ "license": "MIT",
37
+ "homepage": "https://github.com/krstivoja/winden-tokens#readme",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/krstivoja/winden-tokens.git",
41
+ "directory": "bridge"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/krstivoja/winden-tokens/issues"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ }
49
+ }