@aaqiljamal/visual-editor-server 0.2.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/bin/visual-editor-server.cjs +21 -0
- package/dist/chunk-QEI2RGE2.js +1584 -0
- package/dist/chunk-QEI2RGE2.js.map +1 -0
- package/dist/cli.js +83 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +285 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import * as http from 'node:http';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The refusal reasons are structured so the consumer (overlay UI, MCP layer)
|
|
5
|
+
* can branch on them: "show user a 'this className is composed by cn()' badge",
|
|
6
|
+
* "open the file at this location", etc. The `details` field is the human
|
|
7
|
+
* sentence; the `reason` is the machine code.
|
|
8
|
+
*
|
|
9
|
+
* v0.1 only accepts a static string literal at the located JSXOpeningElement.
|
|
10
|
+
* Anything dynamic — cn/clsx/twMerge/cva, template literals, conditionals,
|
|
11
|
+
* spreads, or anything we don't recognize — refuses with a clear reason.
|
|
12
|
+
* This is Principle 1's "determinism preconditions" enforced at the writer.
|
|
13
|
+
*/
|
|
14
|
+
type MutateClassNameRefusalReason = "dynamic-call-expression" | "dynamic-template-literal" | "dynamic-conditional" | "dynamic-other" | "dynamic-uncertain-arg" | "dynamic-spread-arg" | "dynamic-conflict" | "unknown-merger" | "no-classname-attribute" | "no-jsx-at-location" | "token-not-found";
|
|
15
|
+
type MutateAttributeRefusalReason = "no-jsx-at-location" | "no-such-attribute" | "dynamic-value" | "token-not-found" | "parse-error";
|
|
16
|
+
|
|
17
|
+
type ApplyInput = {
|
|
18
|
+
/** Path relative to the configured workspace root (the same root the Babel data-oid plugin used). */
|
|
19
|
+
file: string;
|
|
20
|
+
/** 1-based line number, as Babel reports in `loc.start.line`. */
|
|
21
|
+
line: number;
|
|
22
|
+
/** 0-based column number, as Babel reports in `loc.start.column`. */
|
|
23
|
+
col: number;
|
|
24
|
+
/** Attribute to mutate. Default "className" — token-swap semantics. Anything else uses whole-value swap (src, href, alt, …). */
|
|
25
|
+
attribute?: string;
|
|
26
|
+
/** Exact static class token to swap, e.g. "p-4". For non-className attributes, this is the whole expected current value (or null to skip conflict check). */
|
|
27
|
+
before: string | null;
|
|
28
|
+
/** Replacement, e.g. "p-6". For non-className attributes, this is the whole new value. */
|
|
29
|
+
after: string;
|
|
30
|
+
};
|
|
31
|
+
type ApplyOutcome = {
|
|
32
|
+
ok: true;
|
|
33
|
+
/** Unified diff between original and rewritten source. */
|
|
34
|
+
diff: string;
|
|
35
|
+
/** Absolute path that was written, for logging. */
|
|
36
|
+
absolutePath: string;
|
|
37
|
+
/** For non-className mutations called with before=null, the actual pre-mutation value. Undo uses this. */
|
|
38
|
+
previousValue?: string;
|
|
39
|
+
} | {
|
|
40
|
+
ok: false;
|
|
41
|
+
/** HTTP-shaped status (the http layer maps this 1:1). */
|
|
42
|
+
status: 400 | 403 | 404 | 409 | 500;
|
|
43
|
+
reason: ApplyRefusalReason;
|
|
44
|
+
details: string;
|
|
45
|
+
};
|
|
46
|
+
type ApplyRefusalReason = "invalid-input" | "path-outside-workspace" | "file-not-found" | "read-failed" | "write-failed" | MutateClassNameRefusalReason | MutateAttributeRefusalReason;
|
|
47
|
+
type ApplyOptions = {
|
|
48
|
+
workspaceRoot: string;
|
|
49
|
+
/**
|
|
50
|
+
* If `true`, mutate in memory and return the diff but do NOT write to disk.
|
|
51
|
+
* /propose uses this; /apply doesn't.
|
|
52
|
+
*/
|
|
53
|
+
dryRun: boolean;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* The single entry point for both /apply and /propose. Reads, mutates,
|
|
57
|
+
* conflict-checks (the mutate step's `token-not-found` IS the conflict
|
|
58
|
+
* signal — if the file moved out from under us, the `before` token won't
|
|
59
|
+
* be there anymore), then either writes or skips the write.
|
|
60
|
+
*
|
|
61
|
+
* Returning `ApplyOutcome` instead of throwing keeps the HTTP layer
|
|
62
|
+
* mechanical: every refusal carries its own status code and reason.
|
|
63
|
+
*/
|
|
64
|
+
declare function applyToFile(input: ApplyInput, options: ApplyOptions): Promise<ApplyOutcome>;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Small in-memory deque of recently applied mutations, sized to keep the
|
|
68
|
+
* footprint negligible. Used by the 4th MCP tool (`revert_change`) and
|
|
69
|
+
* the overlay's "undo last apply" affordance.
|
|
70
|
+
*
|
|
71
|
+
* v0.2: persists to `<workspace>/.visual-editor/history.json` so the undo
|
|
72
|
+
* stack survives server restarts. Persistence is fire-and-forget on every
|
|
73
|
+
* mutation — `persistNow()` returns a Promise that tests can await for
|
|
74
|
+
* deterministic checks.
|
|
75
|
+
*/
|
|
76
|
+
type Apply = {
|
|
77
|
+
file: string;
|
|
78
|
+
line: number;
|
|
79
|
+
col: number;
|
|
80
|
+
before: string;
|
|
81
|
+
after: string;
|
|
82
|
+
appliedAt: number;
|
|
83
|
+
};
|
|
84
|
+
declare class RecentApplies {
|
|
85
|
+
private readonly maxSize;
|
|
86
|
+
private buffer;
|
|
87
|
+
private filePath;
|
|
88
|
+
constructor(maxSize?: number);
|
|
89
|
+
/**
|
|
90
|
+
* Wire up disk persistence. If the file exists, its contents become the
|
|
91
|
+
* initial buffer. If it doesn't, we remember the path for future writes.
|
|
92
|
+
*/
|
|
93
|
+
load(filePath: string): Promise<void>;
|
|
94
|
+
push(apply: Apply): void;
|
|
95
|
+
/**
|
|
96
|
+
* Find the most-recent entry matching `key`. If no key is provided, returns
|
|
97
|
+
* the most-recent entry overall (single-step undo). Returns `null` when no
|
|
98
|
+
* matching entry is in the buffer.
|
|
99
|
+
*/
|
|
100
|
+
find(key?: {
|
|
101
|
+
file: string;
|
|
102
|
+
line: number;
|
|
103
|
+
col: number;
|
|
104
|
+
}): Apply | null;
|
|
105
|
+
remove(apply: Apply): void;
|
|
106
|
+
list(): readonly Apply[];
|
|
107
|
+
clear(): void;
|
|
108
|
+
get size(): number;
|
|
109
|
+
/**
|
|
110
|
+
* Write the buffer to disk. No-op when no path has been configured.
|
|
111
|
+
* Safe to `void`-call for fire-and-forget; tests can `await` it for
|
|
112
|
+
* determinism.
|
|
113
|
+
*/
|
|
114
|
+
persistNow(): Promise<void>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
type RevertInput = {
|
|
118
|
+
/** Optional — when omitted, the most-recent apply is reverted. */
|
|
119
|
+
file?: string;
|
|
120
|
+
line?: number;
|
|
121
|
+
col?: number;
|
|
122
|
+
};
|
|
123
|
+
type RevertOutcome = ApplyOutcome | {
|
|
124
|
+
ok: false;
|
|
125
|
+
status: 404;
|
|
126
|
+
reason: "no-recent-apply";
|
|
127
|
+
details: string;
|
|
128
|
+
} | {
|
|
129
|
+
ok: false;
|
|
130
|
+
status: 400;
|
|
131
|
+
reason: "invalid-input";
|
|
132
|
+
details: string;
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Revert a recent apply by swapping `before` and `after` and re-running
|
|
136
|
+
* the deterministic mutation through `applyToFile`. The conflict path
|
|
137
|
+
* (file edited externally between Apply and Revert) is the same as
|
|
138
|
+
* /apply — returns 409 with `token-not-found` if the "after" token no
|
|
139
|
+
* longer sits at line:col.
|
|
140
|
+
*
|
|
141
|
+
* On success, removes the entry from the buffer so re-reverting is not
|
|
142
|
+
* an infinite loop. The user can re-apply by re-driving the gesture.
|
|
143
|
+
*/
|
|
144
|
+
declare function revertToFile(input: RevertInput, options: ApplyOptions, recent: RecentApplies): Promise<RevertOutcome>;
|
|
145
|
+
|
|
146
|
+
type ApplyCssPropertyInput = {
|
|
147
|
+
/** JSX file containing the JSXOpeningElement we resolve from. Workspace-relative. */
|
|
148
|
+
file: string;
|
|
149
|
+
/** 1-based line of the JSXOpeningElement. */
|
|
150
|
+
line: number;
|
|
151
|
+
/** 0-based column of the JSXOpeningElement. */
|
|
152
|
+
col: number;
|
|
153
|
+
/** CSS property to set, e.g. "padding". */
|
|
154
|
+
property: string;
|
|
155
|
+
/** New value, e.g. "1.5rem". */
|
|
156
|
+
value: string;
|
|
157
|
+
};
|
|
158
|
+
type ApplyCssPropertyRefusalReason = "invalid-input" | "path-outside-workspace" | "jsx-file-not-found" | "css-file-not-found" | "read-failed" | "write-failed" | "no-jsx-at-location" | "no-classname-attribute" | "dynamic-classname" | "not-a-css-module" | "unresolved-import" | "parse-error" | "css-parse-error" | "selector-not-found" | "composes-chain" | "invalid-property";
|
|
159
|
+
type ApplyCssPropertyOutcome = {
|
|
160
|
+
ok: true;
|
|
161
|
+
diff: string;
|
|
162
|
+
cssAbsolutePath: string;
|
|
163
|
+
selector: string;
|
|
164
|
+
previousValue: string | null;
|
|
165
|
+
} | {
|
|
166
|
+
ok: false;
|
|
167
|
+
status: 400 | 403 | 404 | 409 | 500;
|
|
168
|
+
reason: ApplyCssPropertyRefusalReason;
|
|
169
|
+
details: string;
|
|
170
|
+
};
|
|
171
|
+
declare function applyCssProperty(input: ApplyCssPropertyInput, options: {
|
|
172
|
+
workspaceRoot: string;
|
|
173
|
+
dryRun: boolean;
|
|
174
|
+
}): Promise<ApplyCssPropertyOutcome>;
|
|
175
|
+
|
|
176
|
+
type ApplyStyledPropertyInput = {
|
|
177
|
+
/** Workspace-relative JSX file containing both the JSX element AND the styled definition. */
|
|
178
|
+
file: string;
|
|
179
|
+
line: number;
|
|
180
|
+
col: number;
|
|
181
|
+
property: string;
|
|
182
|
+
value: string;
|
|
183
|
+
};
|
|
184
|
+
type ApplyStyledPropertyRefusalReason = "invalid-input" | "path-outside-workspace" | "file-not-found" | "read-failed" | "write-failed" | "no-jsx-at-location" | "not-a-styled-component" | "styled-with-interpolation" | "styled-extension-not-supported" | "styled-attrs-not-supported" | "cross-file-styled-not-supported" | "component-not-found" | "parse-error" | "css-parse-error" | "invalid-property";
|
|
185
|
+
type ApplyStyledPropertyOutcome = {
|
|
186
|
+
ok: true;
|
|
187
|
+
diff: string;
|
|
188
|
+
componentName: string;
|
|
189
|
+
previousValue: string | null;
|
|
190
|
+
} | {
|
|
191
|
+
ok: false;
|
|
192
|
+
status: 400 | 403 | 404 | 500;
|
|
193
|
+
reason: ApplyStyledPropertyRefusalReason;
|
|
194
|
+
details: string;
|
|
195
|
+
};
|
|
196
|
+
declare function applyStyledProperty(input: ApplyStyledPropertyInput, options: {
|
|
197
|
+
workspaceRoot: string;
|
|
198
|
+
dryRun: boolean;
|
|
199
|
+
}): Promise<ApplyStyledPropertyOutcome>;
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Single in-memory "what is the user currently looking at" record, set by
|
|
203
|
+
* the overlay on element acquire and read by the MCP `get_selected_element`
|
|
204
|
+
* tool. Intentionally null when no selection is active — the MCP tool
|
|
205
|
+
* returns "no selection" rather than stale state.
|
|
206
|
+
*/
|
|
207
|
+
type Selection = {
|
|
208
|
+
/** Workspace-relative file path from the element's data-oid. */
|
|
209
|
+
file: string;
|
|
210
|
+
/** 1-based line number (Babel convention). */
|
|
211
|
+
line: number;
|
|
212
|
+
/** 0-based column (Babel convention). */
|
|
213
|
+
col: number;
|
|
214
|
+
/** The full data-oid string for traceability. */
|
|
215
|
+
oid: string;
|
|
216
|
+
/** Whole className string (overlay should split if it wants tokens). */
|
|
217
|
+
className: string;
|
|
218
|
+
/** DOM tag name like "div", "button". */
|
|
219
|
+
tagName: string;
|
|
220
|
+
/** Best-guess component name (file basename or Fiber name). */
|
|
221
|
+
componentName: string | null;
|
|
222
|
+
/** How many DOM elements share this data-oid right now (Principle 11). */
|
|
223
|
+
instanceCount: number;
|
|
224
|
+
};
|
|
225
|
+
declare class CurrentSelection {
|
|
226
|
+
private current;
|
|
227
|
+
set(s: Selection | null): void;
|
|
228
|
+
get(): Selection | null;
|
|
229
|
+
clear(): void;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Per-session token. Mounted on startup, kept in memory, and persisted to
|
|
234
|
+
* `<workspace>/.visual-editor/session.json` so the MCP stdio server (a
|
|
235
|
+
* sibling process spawned by Claude Code) can read it without us having to
|
|
236
|
+
* pipe it through a separate transport.
|
|
237
|
+
*
|
|
238
|
+
* Threat model: a random page running on the same dev machine (e.g. an ad
|
|
239
|
+
* iframe, a stale tab on another local port) could POST to the loopback
|
|
240
|
+
* server and clobber files. Requiring an Authorization header that only
|
|
241
|
+
* the overlay (and the MCP server, via the file) knows blocks that vector.
|
|
242
|
+
*
|
|
243
|
+
* Open trade-off in v0.1: GET /token returns the token without auth so
|
|
244
|
+
* the overlay can bootstrap. A drive-by page could fetch it the same way.
|
|
245
|
+
* Production hardening would either (a) inject the token into the page at
|
|
246
|
+
* dev-build time, or (b) pin the Origin/Referer header. Both require
|
|
247
|
+
* dev-server integration that's out of scope here.
|
|
248
|
+
*/
|
|
249
|
+
declare class SessionToken {
|
|
250
|
+
private token;
|
|
251
|
+
private filePath;
|
|
252
|
+
load(workspaceRoot: string): Promise<string>;
|
|
253
|
+
/** Construct the token in-process without touching disk. Used by tests. */
|
|
254
|
+
setInMemory(token: string): void;
|
|
255
|
+
get(): string;
|
|
256
|
+
/**
|
|
257
|
+
* Constant-time compare against the bearer string the client sent. Returns
|
|
258
|
+
* `true` when the lengths match AND every byte is equal. Plain `===` would
|
|
259
|
+
* leak length information through timing.
|
|
260
|
+
*/
|
|
261
|
+
matches(received: string | null): boolean;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
type ServerOptions = {
|
|
265
|
+
/** Absolute path inside which all /apply and /propose writes are constrained. */
|
|
266
|
+
workspaceRoot: string;
|
|
267
|
+
/** Optional pre-built buffer (tests inject their own). */
|
|
268
|
+
recentApplies?: RecentApplies;
|
|
269
|
+
/** Optional pre-built selection state (tests inject their own). */
|
|
270
|
+
currentSelection?: CurrentSelection;
|
|
271
|
+
/** Optional pre-loaded session token (tests inject their own). */
|
|
272
|
+
sessionToken?: SessionToken;
|
|
273
|
+
/**
|
|
274
|
+
* Pinned allowed origins. Empty means any origin (compat with v0.1).
|
|
275
|
+
* Production: pass the dev URL, e.g. `["http://localhost:3000"]`.
|
|
276
|
+
*/
|
|
277
|
+
allowedOrigins?: readonly string[];
|
|
278
|
+
};
|
|
279
|
+
/**
|
|
280
|
+
* Build (but do not start) the local HTTP server. Callers do `.listen(port)`.
|
|
281
|
+
* Tests pass `port: 0` to get a random port; the CLI binds 7790.
|
|
282
|
+
*/
|
|
283
|
+
declare function createServer(options: ServerOptions): http.Server;
|
|
284
|
+
|
|
285
|
+
export { type ApplyCssPropertyInput, type ApplyInput, type ApplyStyledPropertyInput, CurrentSelection, RecentApplies, type RevertInput, type Selection, SessionToken, applyCssProperty, applyStyledProperty, applyToFile, createServer, revertToFile };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CurrentSelection,
|
|
3
|
+
RecentApplies,
|
|
4
|
+
SessionToken,
|
|
5
|
+
applyCssProperty,
|
|
6
|
+
applyStyledProperty,
|
|
7
|
+
applyToFile,
|
|
8
|
+
createServer,
|
|
9
|
+
revertToFile
|
|
10
|
+
} from "./chunk-QEI2RGE2.js";
|
|
11
|
+
export {
|
|
12
|
+
CurrentSelection,
|
|
13
|
+
RecentApplies,
|
|
14
|
+
SessionToken,
|
|
15
|
+
applyCssProperty,
|
|
16
|
+
applyStyledProperty,
|
|
17
|
+
applyToFile,
|
|
18
|
+
createServer,
|
|
19
|
+
revertToFile
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aaqiljamal/visual-editor-server",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Node-side AST mutator for visual-editor: Tailwind className, CSS Modules, styled-components. Used by @aaqiljamal/visual-editor-next as a library, or standalone via the `visual-editor-server` CLI.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"visual-editor-server": "bin/visual-editor-server.cjs"
|
|
17
|
+
},
|
|
18
|
+
"files": ["dist", "bin", "README.md"],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"dev": "tsup --watch",
|
|
22
|
+
"test": "node --import tsx --test 'test/**/*.test.ts'",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"prepublishOnly": "npm run build"
|
|
25
|
+
},
|
|
26
|
+
"keywords": ["visual-editor", "tailwind", "css-modules", "styled-components", "ast"],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"author": "Aaqil Jamal",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/The-Design-Alchemist/visual-editor.git",
|
|
32
|
+
"directory": "packages/server"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/The-Design-Alchemist/visual-editor#readme",
|
|
35
|
+
"bugs": "https://github.com/The-Design-Alchemist/visual-editor/issues",
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=20"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@babel/parser": "^7.27.0",
|
|
44
|
+
"@babel/types": "^7.27.0",
|
|
45
|
+
"@types/diff": "^7.0.2",
|
|
46
|
+
"diff": "^9.0.0",
|
|
47
|
+
"postcss": "^8.5.14",
|
|
48
|
+
"recast": "^0.23.11",
|
|
49
|
+
"tailwind-merge": "^3.6.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^22.10.0",
|
|
53
|
+
"tsup": "^8.3.5",
|
|
54
|
+
"tsx": "^4.19.2",
|
|
55
|
+
"typescript": "^5.6.3"
|
|
56
|
+
}
|
|
57
|
+
}
|