inkbridge 0.1.0-beta.19 → 0.1.0-beta.20
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 +8 -4
- package/bin/inkbridge.mjs +150 -101
- package/code.js +20 -20
- package/package.json +3 -3
- package/scanner/cli.ts +105 -2
- package/scanner/component-scanner.ts +153 -22
- package/scanner/ring-utility-regression.ts +132 -0
- package/src/design-system/design-system.ts +20 -3
- package/src/design-system/story-builder.ts +69 -8
- package/src/main.ts +96 -14
- package/src/render-engine-version.ts +1 -1
- package/src/tailwind/tailwind.ts +14 -96
- package/templates/patch-tokens-route.ts +24 -5
- package/templates/scan-components-route.ts +14 -3
- package/ui.html +101 -13
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Generates native Figma frames and reusable Figma component instances from your T
|
|
|
23
23
|
|
|
24
24
|
- **Figma Desktop** (not the browser — the plugin needs localhost access)
|
|
25
25
|
- **Node.js 18+** and **pnpm**
|
|
26
|
-
- A project with Next.js, Tailwind CSS, and Storybook stories (`.stories.tsx`)
|
|
26
|
+
- A project with Next.js, Tailwind CSS, and Storybook stories (`.stories.tsx` or `.stories.ts`)
|
|
27
27
|
|
|
28
28
|
---
|
|
29
29
|
|
|
@@ -36,11 +36,15 @@ pnpm add -D inkbridge
|
|
|
36
36
|
pnpm exec inkbridge setup
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
`setup`
|
|
40
|
-
|
|
39
|
+
`pnpm exec inkbridge setup` is the only required step after install. It prints exactly what it will change before writing anything (run with `--dry-run` to preview without applying):
|
|
40
|
+
|
|
41
|
+
- Creates `inkbridge.config.json` in your project root (auto-detects component paths from `.storybook/main.*`)
|
|
41
42
|
- Creates the scanner API route at `src/app/api/inkbridge/scan-components/route.ts`
|
|
42
43
|
- Creates the token patch API route at `src/app/api/inkbridge/patch-tokens/route.ts`
|
|
43
44
|
- Adds `inkbridge:dev` and `inkbridge:scan` scripts to your `package.json`
|
|
45
|
+
- Adds `headers()` to your `next.config.*` exposing CORS on `/api/inkbridge/:path*` so the Figma plugin iframe can reach those routes (scoped — your other routes are untouched)
|
|
46
|
+
|
|
47
|
+
Inkbridge does not modify your ESLint config or any other tooling. The recommended `react/forbid-dom-props` rule for inline styles is suggested in "Recommended lint rules" below, but never written by setup.
|
|
44
48
|
|
|
45
49
|
### 2. Load the plugin in Figma Desktop
|
|
46
50
|
|
|
@@ -128,7 +132,7 @@ Edit `inkbridge.config.json` in your project root:
|
|
|
128
132
|
|---|---|---|
|
|
129
133
|
| `componentPaths` | auto-detected | Directories to scan for components |
|
|
130
134
|
| `exclude` | `[]` | Directory or file names to skip |
|
|
131
|
-
| `onlyWithStories` | `true` | Only include components with a `.stories.tsx` file |
|
|
135
|
+
| `onlyWithStories` | `true` | Only include components with a `.stories.tsx` or `.stories.ts` file |
|
|
132
136
|
|
|
133
137
|
The scanner re-reads this file on every scan — no server restart needed.
|
|
134
138
|
|
package/bin/inkbridge.mjs
CHANGED
|
@@ -7,8 +7,9 @@ import { fileURLToPath } from "url";
|
|
|
7
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
8
|
const PACKAGE_DIR = join(__dirname, "..");
|
|
9
9
|
|
|
10
|
-
//
|
|
11
|
-
//
|
|
10
|
+
// `INIT_CWD` is set by package managers to the project where the user
|
|
11
|
+
// invoked the command (vs `process.cwd()` which can be `node_modules/inkbridge`
|
|
12
|
+
// when called via `pnpm exec`). Fall back for direct node invocations.
|
|
12
13
|
const PROJECT_ROOT = process.env.INIT_CWD || process.cwd();
|
|
13
14
|
|
|
14
15
|
const [, , command = "help"] = process.argv;
|
|
@@ -61,9 +62,28 @@ async function patchNextConfig(root) {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
const headersBlock = `
|
|
64
|
-
// Allow Figma plugin UI iframe (null origin) to
|
|
65
|
+
// Allow the Figma plugin UI iframe (null origin) to read the dev
|
|
66
|
+
// server. Two scopes:
|
|
67
|
+
// - /api/inkbridge/* gets full CORS (POST/OPTIONS) for the token
|
|
68
|
+
// patch route. The inkbridge API routes also set per-response
|
|
69
|
+
// CORS headers, so this is mostly belt-and-suspenders for
|
|
70
|
+
// CORS preflight handling.
|
|
71
|
+
// - /:path* gets GET-only CORS so the plugin can fetch the
|
|
72
|
+
// static assets your components reference (e.g.
|
|
73
|
+
// <Image src="/assets/logo.svg">). GET-only on all paths means
|
|
74
|
+
// other origins can READ your local dev server — that's fine
|
|
75
|
+
// for design-system assets and matches what every Storybook
|
|
76
|
+
// and Next.js dev server does in practice.
|
|
65
77
|
async headers() {
|
|
66
78
|
return [
|
|
79
|
+
{
|
|
80
|
+
source: "/api/inkbridge/:path*",
|
|
81
|
+
headers: [
|
|
82
|
+
{ key: "Access-Control-Allow-Origin", value: "*" },
|
|
83
|
+
{ key: "Access-Control-Allow-Methods", value: "GET, POST, OPTIONS" },
|
|
84
|
+
{ key: "Access-Control-Allow-Headers", value: "Content-Type" },
|
|
85
|
+
],
|
|
86
|
+
},
|
|
67
87
|
{
|
|
68
88
|
source: "/:path*",
|
|
69
89
|
headers: [
|
|
@@ -100,124 +120,156 @@ async function patchNextConfig(root) {
|
|
|
100
120
|
}
|
|
101
121
|
|
|
102
122
|
// ---------------------------------------------------------------------------
|
|
103
|
-
//
|
|
123
|
+
// setup — patches package.json + creates scanner route
|
|
104
124
|
// ---------------------------------------------------------------------------
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
125
|
+
function detectEnvironment(root) {
|
|
126
|
+
// Read consumer package.json to detect Next.js, Tailwind, Storybook.
|
|
127
|
+
let pkg = null;
|
|
128
|
+
try { pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); } catch (_e) {}
|
|
129
|
+
const deps = pkg ? { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) } : {};
|
|
130
|
+
const nextConfigCandidates = ["next.config.ts", "next.config.js", "next.config.mjs", "next.config.cjs"];
|
|
131
|
+
const nextConfig = nextConfigCandidates.find(f => existsSync(join(root, f))) || null;
|
|
132
|
+
const storybookMain = ["main.ts", "main.js", "main.mjs", "main.cjs"]
|
|
133
|
+
.map(f => `.storybook/${f}`)
|
|
134
|
+
.find(p => existsSync(join(root, p))) || null;
|
|
135
|
+
const storybookPaths = storybookMain ? detectStorybookPaths(root) : [];
|
|
136
|
+
return {
|
|
137
|
+
nextVersion: deps.next || null,
|
|
138
|
+
nextConfig,
|
|
139
|
+
tailwindVersion: deps.tailwindcss || null,
|
|
140
|
+
storybookMain,
|
|
141
|
+
storybookPaths,
|
|
142
|
+
componentPaths: storybookPaths.length > 0 ? storybookPaths : ["src"],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
108
145
|
|
|
109
|
-
|
|
110
|
-
// version bumps shouldn't shout "RUN SETUP" again — that's misleading.
|
|
111
|
-
const scanRouteExists = existsSync(
|
|
112
|
-
join(PROJECT_ROOT, "src/app/api/inkbridge/scan-components/route.ts")
|
|
113
|
-
);
|
|
146
|
+
function statusGlyph(present) { return present ? "✓" : "✗"; }
|
|
114
147
|
|
|
115
|
-
|
|
116
|
-
console.log(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// First install — the next step is non-obvious and easy to miss in a long
|
|
126
|
-
// install log. Make it loud.
|
|
127
|
-
console.log("");
|
|
128
|
-
console.log(" ┌─────────────────────────────────────────────────────────────┐");
|
|
129
|
-
console.log(" │ ⚠ One more step — wire up the scanner API routes: │");
|
|
130
|
-
console.log(" │ │");
|
|
131
|
-
console.log(" │ pnpm exec inkbridge setup │");
|
|
132
|
-
console.log(" │ │");
|
|
133
|
-
console.log(" │ Without this the plugin will show \"Dev server not │");
|
|
134
|
-
console.log(" │ running\" because the routes it calls don't exist yet. │");
|
|
135
|
-
console.log(" └─────────────────────────────────────────────────────────────┘");
|
|
136
|
-
console.log("");
|
|
137
|
-
console.log(" After setup, load the plugin in Figma Desktop:");
|
|
138
|
-
console.log(" Plugins → Development → Import plugin from manifest");
|
|
139
|
-
console.log(` ${manifestPath}`);
|
|
148
|
+
function printEnvironment(env) {
|
|
149
|
+
console.log(" Detected:");
|
|
150
|
+
console.log(` ${statusGlyph(!!env.nextVersion)} Next.js ${env.nextVersion || "(missing — install `next` first)"}`);
|
|
151
|
+
console.log(` ${statusGlyph(!!env.nextConfig)} next.config ${env.nextConfig || "(missing)"}`);
|
|
152
|
+
console.log(` ${statusGlyph(!!env.tailwindVersion)} Tailwind CSS ${env.tailwindVersion || "(missing — install `tailwindcss` first)"}`);
|
|
153
|
+
console.log(` ${statusGlyph(!!env.storybookMain)} Storybook config ${env.storybookMain || "(none — design generation needs .stories.tsx files to scan)"}`);
|
|
154
|
+
console.log(` • Component paths ${env.componentPaths.join(", ")}${env.storybookPaths.length === 0 ? " (default — edit inkbridge.config.json to customise)" : ""}`);
|
|
140
155
|
console.log("");
|
|
141
156
|
}
|
|
142
157
|
|
|
143
|
-
// ---------------------------------------------------------------------------
|
|
144
|
-
// setup — patches package.json + creates scanner route
|
|
145
|
-
// ---------------------------------------------------------------------------
|
|
146
158
|
async function setup() {
|
|
159
|
+
const dryRun = process.argv.includes("--dry-run");
|
|
147
160
|
const scanRouteDest = join(PROJECT_ROOT, "src/app/api/inkbridge/scan-components/route.ts");
|
|
148
161
|
const scanRouteSrc = join(PACKAGE_DIR, "templates/scan-components-route.ts");
|
|
149
162
|
const patchRouteDest = join(PROJECT_ROOT, "src/app/api/inkbridge/patch-tokens/route.ts");
|
|
150
163
|
const patchRouteSrc = join(PACKAGE_DIR, "templates/patch-tokens-route.ts");
|
|
151
164
|
const pkgPath = join(PROJECT_ROOT, "package.json");
|
|
152
|
-
|
|
153
|
-
// 1. Create inkbridge.config.json
|
|
154
165
|
const inkbridgeCfgPath = join(PROJECT_ROOT, "inkbridge.config.json");
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
166
|
+
|
|
167
|
+
console.log("");
|
|
168
|
+
console.log(` inkbridge setup${dryRun ? " — dry run (no files will be written)" : ""}`);
|
|
169
|
+
console.log("");
|
|
170
|
+
|
|
171
|
+
const env = detectEnvironment(PROJECT_ROOT);
|
|
172
|
+
printEnvironment(env);
|
|
173
|
+
|
|
174
|
+
// ------------------------------------------------------------------
|
|
175
|
+
// Plan phase: figure out which writes are needed.
|
|
176
|
+
// ------------------------------------------------------------------
|
|
177
|
+
const plan = [];
|
|
178
|
+
|
|
179
|
+
if (!existsSync(inkbridgeCfgPath)) {
|
|
180
|
+
plan.push({ kind: "create", path: "inkbridge.config.json", detail: `componentPaths: [${env.componentPaths.join(", ")}]` });
|
|
181
|
+
}
|
|
182
|
+
if (!existsSync(scanRouteDest)) {
|
|
183
|
+
plan.push({ kind: "create", path: "src/app/api/inkbridge/scan-components/route.ts" });
|
|
184
|
+
}
|
|
185
|
+
if (!existsSync(patchRouteDest)) {
|
|
186
|
+
plan.push({ kind: "create", path: "src/app/api/inkbridge/patch-tokens/route.ts" });
|
|
171
187
|
}
|
|
172
188
|
|
|
173
|
-
|
|
174
|
-
if (existsSync(
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
189
|
+
let pkg = null;
|
|
190
|
+
if (existsSync(pkgPath)) {
|
|
191
|
+
try { pkg = JSON.parse(readFileSync(pkgPath, "utf8")); } catch (_e) {}
|
|
192
|
+
}
|
|
193
|
+
const scriptsToAdd = {
|
|
194
|
+
"inkbridge:dev": "next dev",
|
|
195
|
+
"inkbridge:scan": "tsx node_modules/inkbridge/scanner/cli.ts",
|
|
196
|
+
};
|
|
197
|
+
const pkgScriptAdditions = pkg
|
|
198
|
+
? Object.entries(scriptsToAdd).filter(([k]) => !(pkg.scripts && pkg.scripts[k]))
|
|
199
|
+
: [];
|
|
200
|
+
if (pkgScriptAdditions.length > 0) {
|
|
201
|
+
plan.push({
|
|
202
|
+
kind: "modify",
|
|
203
|
+
path: "package.json",
|
|
204
|
+
detail: pkgScriptAdditions.map(([k, v]) => `scripts.${k} = "${v}"`).join("; "),
|
|
205
|
+
});
|
|
180
206
|
}
|
|
181
207
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
208
|
+
const nextConfigSource = env.nextConfig ? await readFile(join(PROJECT_ROOT, env.nextConfig), "utf8") : null;
|
|
209
|
+
const nextConfigNeedsPatch = !!env.nextConfig && !nextConfigSource.includes("Access-Control-Allow-Origin");
|
|
210
|
+
if (nextConfigNeedsPatch) {
|
|
211
|
+
plan.push({
|
|
212
|
+
kind: "modify",
|
|
213
|
+
path: env.nextConfig,
|
|
214
|
+
detail: "headers() → Access-Control-Allow-* on /api/inkbridge/:path*",
|
|
215
|
+
});
|
|
188
216
|
}
|
|
189
217
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
console.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
218
|
+
if (plan.length === 0) {
|
|
219
|
+
console.log(" Nothing to do — setup is already complete.");
|
|
220
|
+
console.log("");
|
|
221
|
+
printPostSetupHint();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
console.log(" Setup will make these changes:");
|
|
226
|
+
for (const item of plan) {
|
|
227
|
+
const prefix = item.kind === "create" ? "+ create" : "~ modify";
|
|
228
|
+
console.log(` ${prefix} ${item.path}${item.detail ? ` (${item.detail})` : ""}`);
|
|
229
|
+
}
|
|
230
|
+
console.log("");
|
|
231
|
+
|
|
232
|
+
if (dryRun) {
|
|
233
|
+
console.log(" Dry run — no changes written. Re-run without --dry-run to apply.");
|
|
234
|
+
console.log("");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ------------------------------------------------------------------
|
|
239
|
+
// Apply phase.
|
|
240
|
+
// ------------------------------------------------------------------
|
|
241
|
+
for (const item of plan) {
|
|
242
|
+
if (item.path === "inkbridge.config.json") {
|
|
243
|
+
const cfg = {
|
|
244
|
+
componentPaths: env.componentPaths,
|
|
245
|
+
exclude: [],
|
|
246
|
+
onlyWithStories: true,
|
|
247
|
+
};
|
|
248
|
+
await writeFile(inkbridgeCfgPath, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
249
|
+
} else if (item.path === "src/app/api/inkbridge/scan-components/route.ts") {
|
|
250
|
+
await mkdir(dirname(scanRouteDest), { recursive: true });
|
|
251
|
+
await copyFile(scanRouteSrc, scanRouteDest);
|
|
252
|
+
} else if (item.path === "src/app/api/inkbridge/patch-tokens/route.ts") {
|
|
253
|
+
await mkdir(dirname(patchRouteDest), { recursive: true });
|
|
254
|
+
await copyFile(patchRouteSrc, patchRouteDest);
|
|
255
|
+
} else if (item.path === "package.json") {
|
|
256
|
+
pkg.scripts = pkg.scripts || {};
|
|
257
|
+
for (const [k, v] of pkgScriptAdditions) pkg.scripts[k] = v;
|
|
208
258
|
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
259
|
+
} else if (env.nextConfig && item.path === env.nextConfig) {
|
|
260
|
+
await patchNextConfig(PROJECT_ROOT);
|
|
261
|
+
continue; // patchNextConfig already prints
|
|
212
262
|
}
|
|
263
|
+
const prefix = item.kind === "create" ? "✓ created" : "✓ modified";
|
|
264
|
+
console.log(` ${prefix} ${item.path}`);
|
|
213
265
|
}
|
|
214
266
|
|
|
215
|
-
|
|
216
|
-
|
|
267
|
+
console.log("");
|
|
268
|
+
printPostSetupHint();
|
|
269
|
+
}
|
|
217
270
|
|
|
218
|
-
|
|
271
|
+
function printPostSetupHint() {
|
|
219
272
|
const manifestPath = resolve(PROJECT_ROOT, "node_modules/inkbridge/manifest.json");
|
|
220
|
-
console.log("");
|
|
221
273
|
console.log(" About the routes:");
|
|
222
274
|
console.log(" src/app/api/inkbridge/scan-components/route.ts");
|
|
223
275
|
console.log(" runs the scanner and returns your component definitions");
|
|
@@ -226,12 +278,12 @@ async function setup() {
|
|
|
226
278
|
console.log(" writes Figma-edited tokens back to your CSS source on Push.");
|
|
227
279
|
console.log(" Don't move or delete these — the plugin won't work without them.");
|
|
228
280
|
console.log("");
|
|
229
|
-
console.log("
|
|
281
|
+
console.log(" Load the plugin in Figma Desktop:");
|
|
230
282
|
console.log(" Plugins → Development → Import plugin from manifest");
|
|
231
283
|
console.log(` ${manifestPath}`);
|
|
232
284
|
console.log("");
|
|
233
285
|
console.log(" Start developing:");
|
|
234
|
-
console.log(" pnpm inkbridge:dev (
|
|
286
|
+
console.log(" pnpm inkbridge:dev (Next.js dev server for the scan + token patch routes)");
|
|
235
287
|
console.log(" pnpm inkbridge:scan (manually re-scan components)");
|
|
236
288
|
console.log("");
|
|
237
289
|
}
|
|
@@ -308,9 +360,6 @@ async function installSkill() {
|
|
|
308
360
|
// dispatch
|
|
309
361
|
// ---------------------------------------------------------------------------
|
|
310
362
|
switch (command) {
|
|
311
|
-
case "postinstall":
|
|
312
|
-
await postinstall();
|
|
313
|
-
break;
|
|
314
363
|
case "setup":
|
|
315
364
|
await setup();
|
|
316
365
|
break;
|