inkbridge 0.1.0-beta.2 → 0.1.0-beta.21
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 +108 -25
- package/bin/inkbridge.mjs +354 -83
- package/code.js +40 -11802
- package/manifest.json +1 -0
- package/package.json +74 -23
- package/scanner/adapter-utils-regression.ts +159 -0
- package/scanner/aspect-percent-position-regression.ts +237 -0
- package/scanner/aspect-ratio-regression.ts +90 -0
- package/scanner/blob-placement-regression.ts +2 -2
- package/scanner/block-cache-regression.ts +195 -0
- package/scanner/bundle-size-regression.ts +50 -0
- package/scanner/child-sizing-matrix-regression.ts +303 -0
- package/scanner/cli.ts +342 -13
- package/scanner/component-scanner.ts +2108 -174
- package/scanner/component-sections-regression.ts +198 -0
- package/scanner/compound-classes-lookup-regression.ts +163 -0
- package/scanner/css-token-reader-regression.ts +7 -6
- package/scanner/css-token-reader.ts +152 -31
- package/scanner/cva-jsx-child-fallback-regression.ts +98 -0
- package/scanner/cva-master-icon-regression.ts +315 -0
- package/scanner/data-attr-prop-alias-regression.ts +129 -0
- package/scanner/explicit-size-root-regression.ts +102 -0
- package/scanner/font-family-extract-regression.ts +113 -0
- package/scanner/font-style-resolver-regression.ts +1 -1
- package/scanner/framework-adapter-shadcn-regression.ts +480 -0
- package/scanner/full-width-matrix-regression.ts +338 -0
- package/scanner/grid-cols-extraction-regression.ts +110 -0
- package/scanner/image-src-collector-regression.ts +204 -0
- package/scanner/inline-flex-regression.ts +235 -0
- package/scanner/input-range-regression.ts +217 -0
- package/scanner/instance-rendering-regression.ts +224 -0
- package/scanner/jsx-prop-unresolved-regression.ts +178 -0
- package/scanner/jsx-text-regression.ts +178 -0
- package/scanner/layout-alignment-regression.ts +108 -0
- package/scanner/layout-flex-regression.ts +90 -0
- package/scanner/layout-mode-regression.ts +71 -0
- package/scanner/layout-sizing-regression.ts +227 -0
- package/scanner/layout-spacing-regression.ts +135 -0
- package/scanner/local-const-className-regression.ts +331 -0
- package/scanner/percent-position-regression.ts +105 -0
- package/scanner/provider-cascade-regression.ts +224 -0
- package/scanner/provider-flatten-regression.ts +235 -0
- package/scanner/radial-gradient-regression.ts +1 -1
- package/scanner/render-prop-parser-regression.ts +161 -0
- package/scanner/ring-utility-regression.ts +153 -0
- package/scanner/sandbox-spread-regression.ts +125 -0
- package/scanner/selection-pressed-regression.ts +241 -0
- package/scanner/size-full-normalization-regression.ts +127 -0
- package/scanner/state-classification-regression.ts +175 -0
- package/scanner/story-diagnostics-regression.ts +216 -0
- package/scanner/story-dimensioning-regression.ts +298 -0
- package/scanner/story-render-strategy-regression.ts +205 -0
- package/scanner/stretch-to-parent-width-regression.ts +147 -0
- package/scanner/svg-fill-parent-regression.ts +98 -0
- package/scanner/svg-group-inheritance-regression.ts +166 -0
- package/scanner/svg-marker-inline-regression.ts +211 -0
- package/scanner/svg-marker-regression.ts +116 -0
- package/scanner/tailwind-parser.ts +46 -4
- package/scanner/text-resize-matrix-regression.ts +173 -0
- package/scanner/transform-math-regression.ts +1 -1
- package/scanner/types.ts +26 -2
- package/src/cache/frame-cache.ts +150 -0
- package/src/cache/index.ts +2 -0
- package/src/{component-defs.ts → components/component-defs.ts} +25 -10
- package/src/{component-gen.ts → components/component-gen.ts} +43 -116
- package/src/components/component-instance.ts +386 -0
- package/src/components/component-library.ts +44 -0
- package/src/components/component-lookup.ts +161 -0
- package/src/components/index.ts +7 -0
- package/src/components/scanner-types.ts +39 -0
- package/src/components/symbol-instance-policy.ts +312 -0
- package/src/design-system/block-cache.ts +130 -0
- package/src/design-system/component-sections.ts +107 -0
- package/src/design-system/cva-inference.ts +187 -0
- package/src/design-system/cva-master.ts +427 -0
- package/src/design-system/cva-utils.ts +29 -0
- package/src/design-system/design-system.ts +334 -0
- package/src/design-system/frame-stabilizers.ts +191 -0
- package/src/design-system/frame-utils.ts +46 -0
- package/src/design-system/generated-node.ts +84 -0
- package/src/design-system/icon-rendering.ts +229 -0
- package/src/design-system/index.ts +13 -0
- package/src/design-system/instance-rendering.ts +307 -0
- package/src/design-system/master-shared.ts +133 -0
- package/src/design-system/node-helpers.ts +237 -0
- package/src/design-system/node-variants.ts +196 -0
- package/src/design-system/non-cva-master.ts +104 -0
- package/src/design-system/portal-handling.ts +138 -0
- package/src/design-system/preview-builder.ts +738 -0
- package/src/{render-context.ts → design-system/render-context.ts} +32 -6
- package/src/design-system/render-prop-parser.ts +50 -0
- package/src/design-system/responsive-resolver.ts +180 -0
- package/src/design-system/selectable-state.ts +157 -0
- package/src/design-system/state-master.ts +267 -0
- package/src/design-system/state-utils.ts +15 -0
- package/src/design-system/story-builder-context.ts +40 -0
- package/src/design-system/story-builder.ts +1322 -0
- package/src/design-system/story-diagnostics.ts +80 -0
- package/src/design-system/story-dimensioning.ts +272 -0
- package/src/design-system/story-frames.ts +400 -0
- package/src/design-system/story-instance.ts +333 -0
- package/src/{story-layout.ts → design-system/story-layout.ts} +2 -2
- package/src/design-system/story-render-strategy.ts +150 -0
- package/src/design-system/story-tree-search.ts +110 -0
- package/src/design-system/symbol-fallback.ts +89 -0
- package/src/design-system/symbol-source.ts +172 -0
- package/src/design-system/table-helpers.ts +56 -0
- package/src/design-system/tag-predicates.ts +99 -0
- package/src/design-system/theme-context.ts +52 -0
- package/src/design-system/typography.ts +100 -0
- package/src/design-system/ui-builder.ts +2676 -0
- package/src/{clip-path-decorative.ts → effects/clip-path-decorative.ts} +11 -11
- package/src/effects/icon-builder.ts +1074 -0
- package/src/effects/index.ts +5 -0
- package/src/effects/portal-panel.ts +369 -0
- package/src/{radial-gradient.ts → effects/radial-gradient.ts} +1 -1
- package/src/framework-adapters/index.ts +47 -0
- package/src/framework-adapters/shadcn.ts +541 -0
- package/src/{github.ts → github/github.ts} +46 -21
- package/src/github/index.ts +1 -0
- package/src/layout/deferred-layout.ts +1556 -0
- package/src/layout/index.ts +24 -0
- package/src/layout/layout-parser.ts +375 -0
- package/src/{layout-utils.ts → layout/layout-utils.ts} +23 -17
- package/src/layout/parser/alignment.ts +54 -0
- package/src/layout/parser/flex.ts +59 -0
- package/src/layout/parser/index.ts +65 -0
- package/src/layout/parser/ir.ts +80 -0
- package/src/layout/parser/layout-mode.ts +57 -0
- package/src/layout/parser/sizing.ts +241 -0
- package/src/layout/parser/spacing-scale.ts +78 -0
- package/src/layout/parser/spacing.ts +134 -0
- package/src/layout/ring-utils.ts +120 -0
- package/src/layout/size-utils.ts +143 -0
- package/src/layout/text-resize-decision.ts +51 -0
- package/src/{width-solver.ts → layout/width-solver.ts} +168 -37
- package/src/main.ts +444 -162
- package/src/{config.ts → plugin/config.ts} +12 -12
- package/src/{dev-server.ts → plugin/dev-server.ts} +3 -3
- package/src/plugin/image-src-collector.ts +52 -0
- package/src/plugin/index.ts +3 -0
- package/src/plugin/packs/index.ts +2 -0
- package/src/{pack-provider.ts → plugin/packs/pack-provider.ts} +12 -12
- package/src/{packs.ts → plugin/packs/packs.ts} +22 -17
- package/src/render-engine-version.ts +2 -0
- package/src/tailwind/adapter-utils.ts +137 -0
- package/src/{class-utils.ts → tailwind/class-utils.ts} +33 -6
- package/src/tailwind/index.ts +8 -0
- package/src/tailwind/jsx-utils.ts +319 -0
- package/src/{node-ir.ts → tailwind/node-ir.ts} +208 -19
- package/src/{responsive-analyzer.ts → tailwind/responsive-analyzer.ts} +32 -2
- package/src/{state-analyzer.ts → tailwind/state-analyzer.ts} +71 -5
- package/src/{tailwind.ts → tailwind/tailwind.ts} +423 -674
- package/src/{utility-resolver.ts → tailwind/utility-resolver.ts} +27 -6
- package/src/{font-style-resolver.ts → text/font-style-resolver.ts} +0 -2
- package/src/text/index.ts +4 -0
- package/src/{inline-text.ts → text/inline-text.ts} +13 -13
- package/src/{text-builder.ts → text/text-builder.ts} +24 -7
- package/src/{text-line.ts → text/text-line.ts} +2 -2
- package/src/{change-detection.ts → tokens/change-detection.ts} +12 -12
- package/src/{color-resolver.ts → tokens/color-resolver.ts} +1 -6
- package/src/{colors.ts → tokens/colors.ts} +13 -6
- package/src/tokens/index.ts +6 -0
- package/src/{token-source.ts → tokens/token-source.ts} +4 -1
- package/src/{tokens.ts → tokens/tokens.ts} +116 -20
- package/src/{variables.ts → tokens/variables.ts} +447 -102
- package/templates/patch-tokens-route.ts +25 -6
- package/templates/scan-components-route.ts +26 -5
- package/ui.html +485 -37
- package/src/component-lookup.ts +0 -82
- package/src/design-system.ts +0 -59
- package/src/icon-builder.ts +0 -607
- package/src/layout-parser.ts +0 -667
- package/src/story-builder.ts +0 -1706
- package/src/ui-builder.ts +0 -1996
- /package/src/{image-cache.ts → cache/image-cache.ts} +0 -0
- /package/src/{blob-placement.ts → effects/blob-placement.ts} +0 -0
- /package/src/{transform-math.ts → tailwind/transform-math.ts} +0 -0
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;
|
|
@@ -39,106 +40,306 @@ function detectStorybookPaths(root) {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
// ---------------------------------------------------------------------------
|
|
42
|
-
//
|
|
43
|
+
// Patch next.config.* to allow the Figma plugin UI (null origin) to fetch
|
|
44
|
+
// assets from the dev server. Without this, <Image> srcs, SVGs, etc. fail CORS.
|
|
43
45
|
// ---------------------------------------------------------------------------
|
|
44
|
-
async function
|
|
45
|
-
const
|
|
46
|
-
|
|
46
|
+
async function patchNextConfig(root) {
|
|
47
|
+
const candidates = ["next.config.ts", "next.config.js", "next.config.mjs", "next.config.cjs"];
|
|
48
|
+
let configPath = null;
|
|
49
|
+
for (const candidate of candidates) {
|
|
50
|
+
const full = join(root, candidate);
|
|
51
|
+
if (existsSync(full)) { configPath = full; break; }
|
|
52
|
+
}
|
|
53
|
+
if (!configPath) {
|
|
54
|
+
console.log(" ~ next.config not found — skipping CORS headers patch");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
47
57
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
const source = await readFile(configPath, "utf8");
|
|
59
|
+
if (source.includes("Access-Control-Allow-Origin")) {
|
|
60
|
+
console.log(" ~ next.config already exposes CORS headers, skipping");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const headersBlock = `
|
|
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.
|
|
77
|
+
async headers() {
|
|
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
|
+
},
|
|
87
|
+
{
|
|
88
|
+
source: "/:path*",
|
|
89
|
+
headers: [
|
|
90
|
+
{ key: "Access-Control-Allow-Origin", value: "*" },
|
|
91
|
+
{ key: "Access-Control-Allow-Methods", value: "GET" },
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
},`;
|
|
96
|
+
|
|
97
|
+
// Inject right after the opening `{` of the config object. Matches either
|
|
98
|
+
// `const nextConfig: NextConfig = {` or `const nextConfig = {` or `module.exports = {`.
|
|
99
|
+
const patterns = [
|
|
100
|
+
/(const\s+\w+\s*(?::\s*\w+\s*)?=\s*)\{/,
|
|
101
|
+
/(module\.exports\s*=\s*)\{/,
|
|
102
|
+
/(export\s+default\s*)\{/,
|
|
103
|
+
];
|
|
104
|
+
let patched = null;
|
|
105
|
+
for (const re of patterns) {
|
|
106
|
+
const match = source.match(re);
|
|
107
|
+
if (!match) continue;
|
|
108
|
+
const idx = match.index + match[0].length;
|
|
109
|
+
patched = source.slice(0, idx) + headersBlock + source.slice(idx);
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!patched) {
|
|
114
|
+
console.log(" ! could not locate config object in " + configPath + " — add CORS headers manually");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await writeFile(configPath, patched, "utf8");
|
|
119
|
+
console.log(" ✓ patched " + configPath.replace(root + "/", "") + " with CORS headers");
|
|
58
120
|
}
|
|
59
121
|
|
|
60
122
|
// ---------------------------------------------------------------------------
|
|
61
123
|
// setup — patches package.json + creates scanner route
|
|
62
124
|
// ---------------------------------------------------------------------------
|
|
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
|
+
}
|
|
145
|
+
|
|
146
|
+
function statusGlyph(present) { return present ? "✓" : "✗"; }
|
|
147
|
+
|
|
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)" : ""}`);
|
|
155
|
+
console.log("");
|
|
156
|
+
}
|
|
157
|
+
|
|
63
158
|
async function setup() {
|
|
64
|
-
const
|
|
159
|
+
const dryRun = process.argv.includes("--dry-run");
|
|
160
|
+
// `--force` re-creates the inkbridge-managed files (route templates,
|
|
161
|
+
// inkbridge.config.json) even when they already exist. Useful when a
|
|
162
|
+
// newer plugin version ships an updated template (e.g. the
|
|
163
|
+
// INKBRIDGE_LOCAL check in the scanner route) and the consumer wants
|
|
164
|
+
// to refresh without hand-editing. Never touches package.json or
|
|
165
|
+
// next.config — those are partial-edit territory and force-rewriting
|
|
166
|
+
// them would clobber consumer state.
|
|
167
|
+
const force = process.argv.includes("--force");
|
|
168
|
+
const scanRouteDest = join(PROJECT_ROOT, "src/app/api/inkbridge/scan-components/route.ts");
|
|
65
169
|
const scanRouteSrc = join(PACKAGE_DIR, "templates/scan-components-route.ts");
|
|
66
|
-
const patchRouteDest = join(PROJECT_ROOT, "src/app/api/
|
|
170
|
+
const patchRouteDest = join(PROJECT_ROOT, "src/app/api/inkbridge/patch-tokens/route.ts");
|
|
67
171
|
const patchRouteSrc = join(PACKAGE_DIR, "templates/patch-tokens-route.ts");
|
|
68
172
|
const pkgPath = join(PROJECT_ROOT, "package.json");
|
|
69
|
-
|
|
70
|
-
// 1. Create inkbridge.config.json
|
|
71
173
|
const inkbridgeCfgPath = join(PROJECT_ROOT, "inkbridge.config.json");
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
174
|
+
|
|
175
|
+
console.log("");
|
|
176
|
+
console.log(` inkbridge setup${dryRun ? " — dry run (no files will be written)" : ""}${force ? " — force (overwrite existing inkbridge files)" : ""}`);
|
|
177
|
+
console.log("");
|
|
178
|
+
|
|
179
|
+
const env = detectEnvironment(PROJECT_ROOT);
|
|
180
|
+
printEnvironment(env);
|
|
181
|
+
|
|
182
|
+
// ------------------------------------------------------------------
|
|
183
|
+
// Plan phase: figure out which writes are needed.
|
|
184
|
+
// ------------------------------------------------------------------
|
|
185
|
+
const plan = [];
|
|
186
|
+
|
|
187
|
+
if (!existsSync(inkbridgeCfgPath) || force) {
|
|
188
|
+
plan.push({
|
|
189
|
+
kind: existsSync(inkbridgeCfgPath) ? "overwrite" : "create",
|
|
190
|
+
path: "inkbridge.config.json",
|
|
191
|
+
detail: `componentPaths: [${env.componentPaths.join(", ")}]`,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (!existsSync(scanRouteDest) || force) {
|
|
195
|
+
plan.push({
|
|
196
|
+
kind: existsSync(scanRouteDest) ? "overwrite" : "create",
|
|
197
|
+
path: "src/app/api/inkbridge/scan-components/route.ts",
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (!existsSync(patchRouteDest) || force) {
|
|
201
|
+
plan.push({
|
|
202
|
+
kind: existsSync(patchRouteDest) ? "overwrite" : "create",
|
|
203
|
+
path: "src/app/api/inkbridge/patch-tokens/route.ts",
|
|
204
|
+
});
|
|
88
205
|
}
|
|
89
206
|
|
|
90
|
-
|
|
91
|
-
if (existsSync(
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
207
|
+
let pkg = null;
|
|
208
|
+
if (existsSync(pkgPath)) {
|
|
209
|
+
try { pkg = JSON.parse(readFileSync(pkgPath, "utf8")); } catch (_e) {}
|
|
210
|
+
}
|
|
211
|
+
const scriptsToAdd = {
|
|
212
|
+
"inkbridge:dev": "next dev",
|
|
213
|
+
"inkbridge:scan": "tsx node_modules/inkbridge/scanner/cli.ts",
|
|
214
|
+
};
|
|
215
|
+
const pkgScriptAdditions = pkg
|
|
216
|
+
? Object.entries(scriptsToAdd).filter(([k]) => !(pkg.scripts && pkg.scripts[k]))
|
|
217
|
+
: [];
|
|
218
|
+
if (pkgScriptAdditions.length > 0) {
|
|
219
|
+
plan.push({
|
|
220
|
+
kind: "modify",
|
|
221
|
+
path: "package.json",
|
|
222
|
+
detail: pkgScriptAdditions.map(([k, v]) => `scripts.${k} = "${v}"`).join("; "),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const nextConfigSource = env.nextConfig ? await readFile(join(PROJECT_ROOT, env.nextConfig), "utf8") : null;
|
|
227
|
+
const nextConfigNeedsPatch = !!env.nextConfig && !nextConfigSource.includes("Access-Control-Allow-Origin");
|
|
228
|
+
if (nextConfigNeedsPatch) {
|
|
229
|
+
plan.push({
|
|
230
|
+
kind: "modify",
|
|
231
|
+
path: env.nextConfig,
|
|
232
|
+
detail: "headers() → Access-Control-Allow-* on /api/inkbridge/:path*",
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Make sure the scanner-output directory is gitignored. The API
|
|
237
|
+
// route writes `.inkbridge/component-definitions.json` on every
|
|
238
|
+
// plugin run; without this entry consumers see the file in their
|
|
239
|
+
// diff after every `pnpm inkbridge:scan`.
|
|
240
|
+
const gitignorePath = join(PROJECT_ROOT, ".gitignore");
|
|
241
|
+
const gitignoreSource = existsSync(gitignorePath)
|
|
242
|
+
? readFileSync(gitignorePath, "utf8")
|
|
243
|
+
: "";
|
|
244
|
+
const gitignoreNeedsPatch = existsSync(join(PROJECT_ROOT, ".git"))
|
|
245
|
+
&& !gitignoreSource.split("\n").some(line => {
|
|
246
|
+
const trimmed = line.trim();
|
|
247
|
+
return trimmed === ".inkbridge" || trimmed === ".inkbridge/";
|
|
248
|
+
});
|
|
249
|
+
if (gitignoreNeedsPatch) {
|
|
250
|
+
plan.push({
|
|
251
|
+
kind: "modify",
|
|
252
|
+
path: ".gitignore",
|
|
253
|
+
detail: "+ .inkbridge/ (scanner output)",
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (plan.length === 0) {
|
|
258
|
+
console.log(" Nothing to do — setup is already complete.");
|
|
259
|
+
console.log("");
|
|
260
|
+
printPostSetupHint();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
console.log(" Setup will make these changes:");
|
|
265
|
+
for (const item of plan) {
|
|
266
|
+
const prefix = item.kind === "create"
|
|
267
|
+
? "+ create"
|
|
268
|
+
: item.kind === "overwrite"
|
|
269
|
+
? "↻ overwrite"
|
|
270
|
+
: "~ modify";
|
|
271
|
+
console.log(` ${prefix} ${item.path}${item.detail ? ` (${item.detail})` : ""}`);
|
|
272
|
+
}
|
|
273
|
+
console.log("");
|
|
274
|
+
|
|
275
|
+
if (dryRun) {
|
|
276
|
+
console.log(" Dry run — no changes written. Re-run without --dry-run to apply.");
|
|
277
|
+
console.log("");
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ------------------------------------------------------------------
|
|
282
|
+
// Apply phase.
|
|
283
|
+
// ------------------------------------------------------------------
|
|
284
|
+
for (const item of plan) {
|
|
285
|
+
if (item.path === "inkbridge.config.json") {
|
|
286
|
+
const cfg = {
|
|
287
|
+
componentPaths: env.componentPaths,
|
|
288
|
+
exclude: [],
|
|
289
|
+
onlyWithStories: true,
|
|
290
|
+
};
|
|
291
|
+
await writeFile(inkbridgeCfgPath, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
292
|
+
} else if (item.path === "src/app/api/inkbridge/scan-components/route.ts") {
|
|
293
|
+
await mkdir(dirname(scanRouteDest), { recursive: true });
|
|
294
|
+
await copyFile(scanRouteSrc, scanRouteDest);
|
|
295
|
+
} else if (item.path === "src/app/api/inkbridge/patch-tokens/route.ts") {
|
|
296
|
+
await mkdir(dirname(patchRouteDest), { recursive: true });
|
|
297
|
+
await copyFile(patchRouteSrc, patchRouteDest);
|
|
298
|
+
} else if (item.path === "package.json") {
|
|
299
|
+
pkg.scripts = pkg.scripts || {};
|
|
300
|
+
for (const [k, v] of pkgScriptAdditions) pkg.scripts[k] = v;
|
|
125
301
|
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
302
|
+
} else if (env.nextConfig && item.path === env.nextConfig) {
|
|
303
|
+
await patchNextConfig(PROJECT_ROOT);
|
|
304
|
+
continue; // patchNextConfig already prints
|
|
305
|
+
} else if (item.path === ".gitignore") {
|
|
306
|
+
const existing = existsSync(gitignorePath)
|
|
307
|
+
? readFileSync(gitignorePath, "utf8")
|
|
308
|
+
: "";
|
|
309
|
+
const needsLeadingNewline = existing.length > 0 && !existing.endsWith("\n");
|
|
310
|
+
const block = (needsLeadingNewline ? "\n" : "")
|
|
311
|
+
+ "\n# Inkbridge scanner output (regenerated on every plugin run)\n.inkbridge/\n";
|
|
312
|
+
await writeFile(gitignorePath, existing + block, "utf8");
|
|
129
313
|
}
|
|
314
|
+
const prefix = item.kind === "create"
|
|
315
|
+
? "✓ created"
|
|
316
|
+
: item.kind === "overwrite"
|
|
317
|
+
? "✓ overwrote"
|
|
318
|
+
: "✓ modified";
|
|
319
|
+
console.log(` ${prefix} ${item.path}`);
|
|
130
320
|
}
|
|
131
321
|
|
|
132
|
-
|
|
322
|
+
console.log("");
|
|
323
|
+
printPostSetupHint();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function printPostSetupHint() {
|
|
133
327
|
const manifestPath = resolve(PROJECT_ROOT, "node_modules/inkbridge/manifest.json");
|
|
328
|
+
console.log(" About the routes:");
|
|
329
|
+
console.log(" src/app/api/inkbridge/scan-components/route.ts");
|
|
330
|
+
console.log(" runs the scanner and returns your component definitions");
|
|
331
|
+
console.log(" (the plugin calls this every time you click Generate).");
|
|
332
|
+
console.log(" src/app/api/inkbridge/patch-tokens/route.ts");
|
|
333
|
+
console.log(" writes Figma-edited tokens back to your CSS source on Push.");
|
|
334
|
+
console.log(" Don't move or delete these — the plugin won't work without them.");
|
|
134
335
|
console.log("");
|
|
135
|
-
console.log("
|
|
336
|
+
console.log(" Load the plugin in Figma Desktop:");
|
|
136
337
|
console.log(" Plugins → Development → Import plugin from manifest");
|
|
137
338
|
console.log(` ${manifestPath}`);
|
|
138
339
|
console.log("");
|
|
139
340
|
console.log(" Start developing:");
|
|
140
|
-
console.log(" pnpm
|
|
141
|
-
console.log(" pnpm
|
|
341
|
+
console.log(" pnpm inkbridge:dev (Next.js dev server for the scan + token patch routes)");
|
|
342
|
+
console.log(" pnpm inkbridge:scan (manually re-scan components)");
|
|
142
343
|
console.log("");
|
|
143
344
|
}
|
|
144
345
|
|
|
@@ -150,22 +351,92 @@ function printPath() {
|
|
|
150
351
|
console.log(manifestPath);
|
|
151
352
|
}
|
|
152
353
|
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
// skill — install the Inkbridge AI skill into the consuming project's
|
|
356
|
+
// .claude/skills/ folder. Maintainer-only convenience: lets developers who
|
|
357
|
+
// extend the plugin (us, the starter, future internal projects) get the
|
|
358
|
+
// canonical agent context without copy-pasting Markdown around.
|
|
359
|
+
//
|
|
360
|
+
// Two modes, picked automatically:
|
|
361
|
+
// - **Sibling mode**: if `../inkbridge/.claude/skills/inkbridge-figma-plugin`
|
|
362
|
+
// exists relative to PROJECT_ROOT, create a symlink. Edits in inkbridge
|
|
363
|
+
// propagate immediately. This is what the starter / co-located projects
|
|
364
|
+
// should use.
|
|
365
|
+
// - **Fetch mode**: otherwise, clone the skill folder from
|
|
366
|
+
// github.com/inkn9ne/inkbridge via degit. Snapshot install — re-run to
|
|
367
|
+
// update. This is what non-co-located projects should use.
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
import { symlink, rm } from "fs/promises";
|
|
370
|
+
|
|
371
|
+
const SKILL_REL_PATH = ".claude/skills/inkbridge-figma-plugin";
|
|
372
|
+
|
|
373
|
+
async function installSkill() {
|
|
374
|
+
const target = join(PROJECT_ROOT, SKILL_REL_PATH);
|
|
375
|
+
const siblingSource = resolve(PROJECT_ROOT, "..", "inkbridge", SKILL_REL_PATH);
|
|
376
|
+
|
|
377
|
+
await mkdir(dirname(target), { recursive: true });
|
|
378
|
+
|
|
379
|
+
if (existsSync(siblingSource)) {
|
|
380
|
+
// Sibling mode — symlink for live updates from the inkbridge repo
|
|
381
|
+
if (existsSync(target)) {
|
|
382
|
+
console.log(` ~ ${SKILL_REL_PATH} already exists — removing to relink`);
|
|
383
|
+
await rm(target, { recursive: true, force: true });
|
|
384
|
+
}
|
|
385
|
+
const relativeSource = resolve(siblingSource);
|
|
386
|
+
await symlink(relativeSource, target, "dir");
|
|
387
|
+
console.log(` ✓ symlinked ${SKILL_REL_PATH} → ${relativeSource}`);
|
|
388
|
+
console.log("");
|
|
389
|
+
console.log(" Sibling-mode install. Edits in the inkbridge repo's");
|
|
390
|
+
console.log(" .claude/skills/inkbridge-figma-plugin/ will propagate here");
|
|
391
|
+
console.log(" immediately. Reload Claude Code to pick up the skill.");
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Fetch mode — pull the current main from GitHub via degit
|
|
396
|
+
if (existsSync(target)) {
|
|
397
|
+
console.log(` ~ ${SKILL_REL_PATH} already exists — re-fetching to update`);
|
|
398
|
+
await rm(target, { recursive: true, force: true });
|
|
399
|
+
}
|
|
400
|
+
const { spawn } = await import("child_process");
|
|
401
|
+
const args = ["degit", "inkn9ne/inkbridge/.claude/skills/inkbridge-figma-plugin", target];
|
|
402
|
+
console.log(` ↓ npx ${args.join(" ")}`);
|
|
403
|
+
await new Promise((resolve, reject) => {
|
|
404
|
+
const child = spawn("npx", args, { stdio: "inherit" });
|
|
405
|
+
child.on("exit", code => (code === 0 ? resolve() : reject(new Error(`degit exited ${code}`))));
|
|
406
|
+
child.on("error", reject);
|
|
407
|
+
});
|
|
408
|
+
console.log(` ✓ installed ${SKILL_REL_PATH} from inkn9ne/inkbridge`);
|
|
409
|
+
console.log("");
|
|
410
|
+
console.log(" Fetch-mode install (snapshot from main). Re-run");
|
|
411
|
+
console.log(" `pnpm exec inkbridge skill install` to update.");
|
|
412
|
+
}
|
|
413
|
+
|
|
153
414
|
// ---------------------------------------------------------------------------
|
|
154
415
|
// dispatch
|
|
155
416
|
// ---------------------------------------------------------------------------
|
|
156
417
|
switch (command) {
|
|
157
|
-
case "postinstall":
|
|
158
|
-
await postinstall();
|
|
159
|
-
break;
|
|
160
418
|
case "setup":
|
|
161
419
|
await setup();
|
|
162
420
|
break;
|
|
163
421
|
case "path":
|
|
164
422
|
printPath();
|
|
165
423
|
break;
|
|
424
|
+
case "skill": {
|
|
425
|
+
const sub = process.argv[3];
|
|
426
|
+
if (sub === "install") {
|
|
427
|
+
await installSkill();
|
|
428
|
+
} else {
|
|
429
|
+
console.log("Usage:");
|
|
430
|
+
console.log(" inkbridge skill install Install the AI skill into .claude/skills/");
|
|
431
|
+
console.log(" (symlinks to ../inkbridge if co-located,");
|
|
432
|
+
console.log(" otherwise fetches via degit from GitHub)");
|
|
433
|
+
}
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
166
436
|
default:
|
|
167
437
|
console.log("Usage:");
|
|
168
|
-
console.log(" inkbridge setup
|
|
169
|
-
console.log(" inkbridge path
|
|
438
|
+
console.log(" inkbridge setup Wire up scanner/token-patch routes + scripts (run once after install)");
|
|
439
|
+
console.log(" inkbridge path Print the manifest.json path for Figma Desktop");
|
|
440
|
+
console.log(" inkbridge skill install Install the AI development skill (maintainers only)");
|
|
170
441
|
break;
|
|
171
442
|
}
|