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 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` does three things automatically:
40
- - Creates `inkbridge.config.json` in your project root (auto-detects component paths from `.storybook/main.ts`)
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
- // When running via postinstall, INIT_CWD is the consumer project root.
11
- // Fall back to process.cwd() for direct invocations.
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 fetch static assets
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
- // postinstallcalled automatically after `pnpm add -D inkbridge`
123
+ // setuppatches package.json + creates scanner route
104
124
  // ---------------------------------------------------------------------------
105
- async function postinstall() {
106
- const pkg = JSON.parse(await readFile(join(PACKAGE_DIR, "package.json"), "utf8"));
107
- const manifestPath = join(PROJECT_ROOT, "node_modules/inkbridge/manifest.json");
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
- // If setup has already run, the scanner route file exists. Re-installs and
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
- console.log("");
116
- console.log(` ✓ inkbridge v${pkg.version} installed`);
117
-
118
- if (scanRouteExists) {
119
- console.log("");
120
- console.log(" Setup already done. Run `pnpm inkbridge:dev` and open the plugin in Figma.");
121
- console.log("");
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
- if (existsSync(inkbridgeCfgPath)) {
156
- console.log(" ~ inkbridge.config.json already exists, skipping");
157
- } else {
158
- const detectedPaths = detectStorybookPaths(PROJECT_ROOT);
159
- const componentPaths = detectedPaths.length > 0 ? detectedPaths : ["src"];
160
- const cfg = {
161
- componentPaths,
162
- exclude: [],
163
- onlyWithStories: true,
164
- };
165
- await writeFile(inkbridgeCfgPath, JSON.stringify(cfg, null, 2) + "\n", "utf8");
166
- if (detectedPaths.length > 0) {
167
- console.log(` ✓ created inkbridge.config.json (detected paths: ${detectedPaths.join(", ")})`);
168
- } else {
169
- console.log(" created inkbridge.config.json (defaulting to src/ edit to customise)");
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
- // 3. Copy scanner route
174
- if (existsSync(scanRouteDest)) {
175
- console.log(" ~ scanner route already exists, skipping: src/app/api/inkbridge/scan-components/route.ts");
176
- } else {
177
- await mkdir(dirname(scanRouteDest), { recursive: true });
178
- await copyFile(scanRouteSrc, scanRouteDest);
179
- console.log(" created src/app/api/inkbridge/scan-components/route.ts");
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
- if (existsSync(patchRouteDest)) {
183
- console.log(" ~ token patch route already exists, skipping: src/app/api/inkbridge/patch-tokens/route.ts");
184
- } else {
185
- await mkdir(dirname(patchRouteDest), { recursive: true });
186
- await copyFile(patchRouteSrc, patchRouteDest);
187
- console.log(" ✓ created src/app/api/inkbridge/patch-tokens/route.ts");
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
- // 4. Patch package.json scripts
191
- if (!existsSync(pkgPath)) {
192
- console.warn(" ! package.json not found at project root — skipping script injection");
193
- } else {
194
- const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
195
- pkg.scripts = pkg.scripts || {};
196
- const toAdd = {
197
- "inkbridge:dev": "next dev",
198
- "inkbridge:scan": "tsx node_modules/inkbridge/scanner/cli.ts",
199
- };
200
- const added = [];
201
- for (const [k, v] of Object.entries(toAdd)) {
202
- if (!pkg.scripts[k]) {
203
- pkg.scripts[k] = v;
204
- added.push(k);
205
- }
206
- }
207
- if (added.length > 0) {
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
- for (const k of added) console.log(` ✓ added script: ${k}`);
210
- } else {
211
- console.log(" ~ scripts already present, skipping");
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
- // 5. Patch next.config to expose CORS headers for the Figma plugin iframe
216
- await patchNextConfig(PROJECT_ROOT);
267
+ console.log("");
268
+ printPostSetupHint();
269
+ }
217
270
 
218
- // 6. Print manifest path
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(" Setup complete. Load the plugin in Figma Desktop:");
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 (starts Next.js dev server for scan + token patch routes)");
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;