@vesk/adapter 0.2.9 → 0.2.11

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.
Files changed (42) hide show
  1. package/dist/client-bundle.d.ts +29 -0
  2. package/dist/client-bundle.d.ts.map +1 -1
  3. package/dist/client-bundle.js +333 -52
  4. package/dist/dev-api.d.ts +78 -0
  5. package/dist/dev-api.d.ts.map +1 -0
  6. package/dist/dev-api.js +338 -0
  7. package/dist/dev-config.d.ts +48 -0
  8. package/dist/dev-config.d.ts.map +1 -0
  9. package/dist/dev-config.js +964 -0
  10. package/dist/dev-server.d.ts +85 -0
  11. package/dist/dev-server.d.ts.map +1 -1
  12. package/dist/dev-server.js +329 -8
  13. package/dist/error-codeframe.d.ts +23 -0
  14. package/dist/error-codeframe.d.ts.map +1 -0
  15. package/dist/error-codeframe.js +127 -0
  16. package/dist/error-tips.d.ts +7 -0
  17. package/dist/error-tips.d.ts.map +1 -0
  18. package/dist/error-tips.js +91 -0
  19. package/dist/hmr-utils.d.ts +14 -0
  20. package/dist/hmr-utils.d.ts.map +1 -0
  21. package/dist/hmr-utils.js +56 -0
  22. package/dist/hmr.d.ts +40 -0
  23. package/dist/hmr.d.ts.map +1 -1
  24. package/dist/hmr.js +139 -20
  25. package/dist/index.d.ts +37 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +105 -22
  28. package/dist/paths.d.ts +8 -0
  29. package/dist/paths.d.ts.map +1 -1
  30. package/dist/paths.js +32 -0
  31. package/dist/platform-handler.d.ts.map +1 -1
  32. package/dist/platform-handler.js +2 -1
  33. package/dist/plugins.d.ts +147 -0
  34. package/dist/plugins.d.ts.map +1 -0
  35. package/dist/plugins.js +1109 -0
  36. package/dist/prod-server.d.ts.map +1 -1
  37. package/dist/prod-server.js +43 -9
  38. package/dist/ssr-function.d.ts.map +1 -1
  39. package/dist/ssr-function.js +14 -2
  40. package/dist/types.d.ts +1 -1
  41. package/dist/types.d.ts.map +1 -1
  42. package/package.json +4 -4
@@ -0,0 +1,964 @@
1
+ /**
2
+ * Browser-config view of `vesk.config.{ts,js}` for the DevTools (B1).
3
+ *
4
+ * This is the Dev-Server-side bridge between the browser panel and the real
5
+ * config file. It reads the config source + parses it (same transpile+inject
6
+ * trick the CLI main entry uses), and writes back a validated source so an
7
+ * invalid config never clobbers the file. `applyConfigToggle` edits a single
8
+ * key in the `defineConfig({...})` object literal, preserving formatting.
9
+ *
10
+ * Path containment + validation are enforced here; the dev panel router
11
+ * applies capability/permission checks before these are reachable.
12
+ */
13
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
14
+ import { tmpdir } from 'node:os';
15
+ import { resolve, dirname, join } from 'node:path';
16
+ import { defineConfig, validateConfig, preset } from '@vesk/compiler/src/config';
17
+ import { parse as parseVsk } from '@vesk/compiler/src/parser';
18
+ /** Locate the project's config file (vesk.config.ts preferred, then .js). */
19
+ export function findConfigFile(projectDir) {
20
+ const ts = resolve(projectDir, 'vesk.config.ts');
21
+ if (existsSync(ts))
22
+ return { path: ts, isTs: true };
23
+ const js = resolve(projectDir, 'vesk.config.js');
24
+ if (existsSync(js))
25
+ return { path: js, isTs: false };
26
+ return { path: null, isTs: false };
27
+ }
28
+ function resolveConfig(raw) {
29
+ const config = (typeof defineConfig === 'function' ? defineConfig(raw) : raw);
30
+ if (typeof validateConfig === 'function')
31
+ validateConfig(config);
32
+ return config;
33
+ }
34
+ /**
35
+ * Transpile+parse a config source into a validated VeskConfig. Shared by read
36
+ * and write paths so they agree on what "valid" means. Host code is executed
37
+ * in an isolated temp module so edits re-evaluate fresh on every call.
38
+ */
39
+ export async function parseConfigSource(source, isTs, projectDir) {
40
+ if (!source.trim())
41
+ return {};
42
+ let js = source;
43
+ if (isTs) {
44
+ const { transpile } = (await import('typescript'));
45
+ js = transpile(source, { module: 99, target: 99 });
46
+ js = js.replace(/import\s+\{[^}]*\}\s*from\s+['"]@vesk\/compiler['"]\s*;?\s*/g, '');
47
+ js = `const { defineConfig, definePlugin, preset } = globalThis.__vesk_inject;\n` + js;
48
+ }
49
+ // Evaluate the config from a module rooted INSIDE the project (`.vesk/`) so
50
+ // bare package imports in `vesk.config.ts` (e.g. `@vesk/plugin-tailwind`)
51
+ // resolve against the project's `node_modules` rather than the OS temp dir.
52
+ const base = projectDir
53
+ ? (mkdirSync(join(projectDir, '.vesk'), { recursive: true }), join(projectDir, '.vesk'))
54
+ : undefined;
55
+ const dir = base ? mkdtempSync(join(base, 'cfg-')) : mkdtempSync(join(tmpdir(), 'vesk-cfg-'));
56
+ const tmpFile = join(dir, 'config.mjs');
57
+ try {
58
+ writeFileSync(tmpFile, js, 'utf-8');
59
+ globalThis.__vesk_inject = {
60
+ defineConfig,
61
+ // Pass-through like the real definePlugin (which validates + returns
62
+ // its argument): a stub returning {} would strip the plugin's `name`
63
+ // and make validateConfig reject the file (GET /__vesk/config → 500).
64
+ definePlugin: (p) => p,
65
+ preset,
66
+ };
67
+ const mod = await import(`${tmpFile}?t=${Date.now()}`);
68
+ const raw = (mod.default ?? mod);
69
+ const cfg = (typeof raw === 'function' ? raw() : raw);
70
+ return resolveConfig(cfg);
71
+ }
72
+ finally {
73
+ delete globalThis.__vesk_inject;
74
+ try {
75
+ rmSync(dir, { recursive: true, force: true });
76
+ }
77
+ catch { /* best-effort */ }
78
+ }
79
+ }
80
+ /** Read + parse the project config. Throws on an invalid config. */
81
+ export async function readConfig(projectDir) {
82
+ const { path, isTs } = findConfigFile(projectDir);
83
+ if (!path)
84
+ return { path: null, exists: false, source: '', config: {} };
85
+ const source = readFileSync(path, 'utf-8');
86
+ const config = await parseConfigSource(source, isTs, projectDir);
87
+ return { path, exists: true, source, config };
88
+ }
89
+ /**
90
+ * Write a full new config source back, after validation. Guarantees an invalid
91
+ * config never clobbers the file (throws before writing).
92
+ */
93
+ export async function writeConfigSource(projectDir, source) {
94
+ const { path, isTs } = findConfigFile(projectDir);
95
+ const target = path || resolve(projectDir, 'vesk.config.ts');
96
+ await parseConfigSource(source, isTs, projectDir); // validate BEFORE writing
97
+ mkdirSync(dirname(target), { recursive: true });
98
+ writeFileSync(target, source, 'utf-8');
99
+ const config = await parseConfigSource(source, isTs, projectDir);
100
+ return { path: target, exists: true, source, config };
101
+ }
102
+ /**
103
+ * Apply a single `key` → `value` toggle to a `vesk.config.ts` source by
104
+ * editing the object literal passed to `defineConfig(...)`, preserving all
105
+ * other formatting/comments. Returns the new source, or `null` when there is
106
+ * no safe literal-edit point (caller falls back to the direct editor).
107
+ */
108
+ export function applyConfigToggle(source, key, value) {
109
+ if (typeof key !== 'string' || !key)
110
+ return null;
111
+ if (/^[a-zA-Z_$][\w$]*$/.test(key) === false)
112
+ return null;
113
+ const marker = 'defineConfig(';
114
+ const idx = source.indexOf(marker);
115
+ if (idx === -1)
116
+ return null;
117
+ const openBrace = source.indexOf('{', idx + marker.length);
118
+ if (openBrace === -1)
119
+ return null;
120
+ const end = findMatchingBrace(source, openBrace);
121
+ if (end === -1)
122
+ return null;
123
+ const obj = tryParseJsObject(source.slice(openBrace + 1, end));
124
+ if (obj === null)
125
+ return null;
126
+ obj[key] = normalizeDisplayValue(value);
127
+ return source.slice(0, openBrace + 1) + serializeObject(obj) + source.slice(end);
128
+ }
129
+ /** Coerce UI-provided values to literal-safe JSON-ish equivalents. */
130
+ function normalizeDisplayValue(v) {
131
+ if (v === undefined)
132
+ return null;
133
+ if (typeof v === 'number' || typeof v === 'boolean' || v === null)
134
+ return v;
135
+ if (Array.isArray(v))
136
+ return v.map(normalizeDisplayValue);
137
+ if (typeof v === 'object') {
138
+ const o = {};
139
+ for (const k of Object.keys(v))
140
+ o[k] = normalizeDisplayValue(v[k]);
141
+ return o;
142
+ }
143
+ return String(v);
144
+ }
145
+ function findMatchingBrace(src, openIdx) {
146
+ let inStr = null;
147
+ let esc = false;
148
+ let depth = 0;
149
+ for (let i = openIdx; i < src.length; i++) {
150
+ const c = src[i];
151
+ if (inStr) {
152
+ if (esc) {
153
+ esc = false;
154
+ continue;
155
+ }
156
+ if (c === '\\') {
157
+ esc = true;
158
+ continue;
159
+ }
160
+ if (c === inStr)
161
+ inStr = null;
162
+ continue;
163
+ }
164
+ if (c === '"' || c === "'" || c === '`') {
165
+ inStr = c;
166
+ continue;
167
+ }
168
+ if (c === '{')
169
+ depth++;
170
+ else if (c === '}') {
171
+ depth--;
172
+ if (depth === 0)
173
+ return i;
174
+ }
175
+ }
176
+ return -1;
177
+ }
178
+ function tryParseJsObject(src) {
179
+ let i = 0;
180
+ const out = {};
181
+ while (i < src.length) {
182
+ while (i < src.length && /\s/.test(src[i]))
183
+ i++;
184
+ if (i >= src.length)
185
+ break;
186
+ const c = src[i];
187
+ if (c === ',' || c === ';') {
188
+ i++;
189
+ continue;
190
+ }
191
+ const keyStart = i;
192
+ let key;
193
+ if (c === '"' || c === "'" || c === '`') {
194
+ let j = i + 1;
195
+ let k = '';
196
+ while (j < src.length && (src[j] !== c || src[j - 1] === '\\')) {
197
+ k += src[j];
198
+ j++;
199
+ }
200
+ key = k;
201
+ i = j + 1;
202
+ }
203
+ else {
204
+ while (i < src.length && !/[:=\s]/.test(src[i]))
205
+ i++;
206
+ key = src.slice(keyStart, i).trim();
207
+ }
208
+ while (i < src.length && /\s/.test(src[i]))
209
+ i++;
210
+ if (src[i] === '=')
211
+ return null; // `key = value` — not a literal object; bail
212
+ if (src[i] !== ':')
213
+ return null;
214
+ i++;
215
+ while (i < src.length && /\s/.test(src[i]))
216
+ i++;
217
+ const val = parseLiteralValue(src, i);
218
+ if (val === null || val.__invalid)
219
+ return null;
220
+ out[key.replace(/^["'`]|["'`]$/g, '')] = val.value;
221
+ i = val.next;
222
+ }
223
+ return out;
224
+ }
225
+ function parseLiteralValue(src, i) {
226
+ const c = src[i];
227
+ if (c === undefined)
228
+ return null;
229
+ if (c === '{') {
230
+ const end = findMatchingBrace(src, i);
231
+ if (end === -1)
232
+ return { value: undefined, next: i, __invalid: true };
233
+ const obj = tryParseJsObject(src.slice(i + 1, end));
234
+ if (obj === null)
235
+ return { value: undefined, next: i, __invalid: true };
236
+ return { value: obj, next: end + 1 };
237
+ }
238
+ if (c === '[') {
239
+ const vals = [];
240
+ let j = i + 1;
241
+ while (j < src.length) {
242
+ while (j < src.length && /\s/.test(src[j]))
243
+ j++;
244
+ const cc = src[j];
245
+ if (cc === ']') {
246
+ j++;
247
+ break;
248
+ }
249
+ if (cc === ',') {
250
+ j++;
251
+ continue;
252
+ }
253
+ const r = parseLiteralValue(src, j);
254
+ if (r === null || r.__invalid)
255
+ return { value: undefined, next: i, __invalid: true };
256
+ vals.push(r.value);
257
+ j = r.next;
258
+ }
259
+ return { value: vals, next: j };
260
+ }
261
+ if (c === '"' || c === "'" || c === '`') {
262
+ const q = c;
263
+ let j = i + 1;
264
+ let s = '';
265
+ while (j < src.length && (src[j] !== q || src[j - 1] === '\\')) {
266
+ s += src[j];
267
+ j++;
268
+ }
269
+ return { value: s, next: j + 1 };
270
+ }
271
+ let j = i;
272
+ while (j < src.length && /[\w.\-]/.test(src[j]))
273
+ j++;
274
+ const token = src.slice(i, j);
275
+ if (token === 'true')
276
+ return { value: true, next: j };
277
+ if (token === 'false')
278
+ return { value: false, next: j };
279
+ if (token === 'null')
280
+ return { value: null, next: j };
281
+ if (/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(token))
282
+ return { value: Number(token), next: j };
283
+ return { value: undefined, next: i, __invalid: true };
284
+ }
285
+ function serializeObject(obj) {
286
+ return Object.keys(obj)
287
+ .map((k) => `${isBareKey(k) ? k : JSON.stringify(k)}: ${serializeValue(obj[k])}`)
288
+ .join(', ');
289
+ }
290
+ function isBareKey(k) {
291
+ return /^[a-zA-Z_$][\w$]*$/.test(k);
292
+ }
293
+ function serializeValue(v) {
294
+ if (v === null)
295
+ return 'null';
296
+ if (typeof v === 'string')
297
+ return JSON.stringify(v);
298
+ if (Array.isArray(v))
299
+ return '[' + v.map(serializeValue).join(', ') + ']';
300
+ if (typeof v === 'object')
301
+ return '{ ' + serializeObject(v) + ' }';
302
+ return String(v);
303
+ }
304
+ // ─── vesk.config.ts plugin import / plugins[] surgical editors ─────────────
305
+ /** Derive a safe import identifier for a package spec. `importNameForPackage('@vesk/plugin-tailwind')` -> `tailwindcss` (known) else `myPlugin` etc. */
306
+ export function importNameForPackage(pkg) {
307
+ if (pkg === '@vesk/plugin-tailwind')
308
+ return 'tailwindcss';
309
+ const last = (pkg.split('/').pop() || pkg).replace(/^plugin-/, '');
310
+ const parts = last.split(/[^A-Za-z0-9]+/).filter(Boolean);
311
+ if (parts.length === 0)
312
+ return 'plugin';
313
+ const camel = parts
314
+ .map((p, i) => (i === 0 ? p.toLowerCase() : p.charAt(0).toUpperCase() + p.slice(1).toLowerCase()))
315
+ .join('');
316
+ let name = camel.replace(/[^A-Za-z0-9_$]/g, '');
317
+ if (!name)
318
+ name = 'plugin';
319
+ if (!/^[A-Za-z_$]/.test(name))
320
+ name = '_' + name;
321
+ // avoid reserved-ish
322
+ if (/^(import|export|default|const|let|var)$/.test(name))
323
+ name = name + 'Plugin';
324
+ return name;
325
+ }
326
+ function escapeRegExp(s) {
327
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
328
+ }
329
+ // ─── AST helpers (acorn + tsPlugin) — preferred over regex for syntax analysis ─
330
+ function parseAstOrNull(source) {
331
+ try {
332
+ // `parseVsk` is the compiler's base parser (acorn + TypeScript + Vesk JSX).
333
+ // It handles plain TS/JS config files as well as Vesk syntax, and reports
334
+ // ranges/locs needed for surgical edits.
335
+ return parseVsk(source, { sourceType: 'module' });
336
+ }
337
+ catch {
338
+ return null;
339
+ }
340
+ }
341
+ function findLastImportEnd(src) {
342
+ const ast = parseAstOrNull(src);
343
+ if (ast && Array.isArray(ast.body)) {
344
+ let last = -1;
345
+ for (const node of ast.body) {
346
+ if (node.type === 'ImportDeclaration' && typeof node.end === 'number') {
347
+ let end = node.end;
348
+ // include trailing semicolon already in `end`, then one line break
349
+ if (end < src.length && src[end] === '\r' && src[end + 1] === '\n')
350
+ end += 2;
351
+ else if (end < src.length && (src[end] === '\n' || src[end] === '\r'))
352
+ end += 1;
353
+ else if (end < src.length && src[end] === ';') {
354
+ end += 1;
355
+ if (src[end] === '\r' && src[end + 1] === '\n')
356
+ end += 2;
357
+ else if (src[end] === '\n')
358
+ end += 1;
359
+ }
360
+ if (end > last)
361
+ last = end;
362
+ }
363
+ }
364
+ if (last !== -1)
365
+ return last;
366
+ }
367
+ // fallback: line scan without regex
368
+ let last = -1;
369
+ let idx = 0;
370
+ while (idx < src.length) {
371
+ const nl = src.indexOf('\n', idx);
372
+ const lineEnd = nl === -1 ? src.length : nl + 1;
373
+ const line = src.slice(idx, lineEnd);
374
+ const t = line.trim();
375
+ if (t.startsWith('import ') && t.includes(' from ') && (t.includes("'") || t.includes('"'))) {
376
+ last = lineEnd;
377
+ }
378
+ if (nl === -1)
379
+ break;
380
+ idx = lineEnd;
381
+ }
382
+ return last;
383
+ }
384
+ function findMatchingBracket(src, openIdx, openChar = '[', closeChar = ']') {
385
+ let inStr = null;
386
+ let esc = false;
387
+ let depth = 0;
388
+ for (let i = openIdx; i < src.length; i++) {
389
+ const c = src[i];
390
+ if (inStr) {
391
+ if (esc) {
392
+ esc = false;
393
+ continue;
394
+ }
395
+ if (c === '\\') {
396
+ esc = true;
397
+ continue;
398
+ }
399
+ if (c === inStr)
400
+ inStr = null;
401
+ continue;
402
+ }
403
+ if (c === '"' || c === "'" || c === '`') {
404
+ inStr = c;
405
+ continue;
406
+ }
407
+ if (c === '/' && src[i + 1] === '/') {
408
+ const nl = src.indexOf('\n', i);
409
+ i = nl === -1 ? src.length : nl;
410
+ continue;
411
+ }
412
+ if (c === '/' && src[i + 1] === '*') {
413
+ const end = src.indexOf('*/', i + 2);
414
+ i = end === -1 ? src.length : end + 1;
415
+ continue;
416
+ }
417
+ if (c === openChar)
418
+ depth++;
419
+ else if (c === closeChar) {
420
+ depth--;
421
+ if (depth === 0)
422
+ return i;
423
+ }
424
+ }
425
+ return -1;
426
+ }
427
+ function findPluginImportAst(src, pkg) {
428
+ const ast = parseAstOrNull(src);
429
+ if (!ast)
430
+ return null;
431
+ for (const node of ast.body) {
432
+ if (node.type === 'ImportDeclaration' && node.source && node.source.value === pkg) {
433
+ const spec = node.specifiers && node.specifiers[0];
434
+ if (spec && spec.type === 'ImportDefaultSpecifier' && spec.local && typeof spec.local.name === 'string') {
435
+ return { name: spec.local.name, start: node.start, end: node.end };
436
+ }
437
+ // `import { foo } from 'pkg'` or `import * as foo`
438
+ if (spec && spec.local && typeof spec.local.name === 'string') {
439
+ return { name: spec.local.name, start: node.start, end: node.end };
440
+ }
441
+ }
442
+ }
443
+ return null;
444
+ }
445
+ function findPluginsArrayBounds(src) {
446
+ const ast = parseAstOrNull(src);
447
+ if (ast) {
448
+ for (const node of ast.body) {
449
+ if (node.type === 'ExportDefaultDeclaration' && node.declaration) {
450
+ let decl = node.declaration;
451
+ let obj = null;
452
+ if (decl.type === 'CallExpression' && decl.callee && decl.callee.type === 'Identifier' && decl.callee.name === 'defineConfig' && decl.arguments && decl.arguments.length > 0) {
453
+ const arg = decl.arguments[0];
454
+ if (arg && arg.type === 'ObjectExpression')
455
+ obj = arg;
456
+ else if (arg && arg.type === 'ArrowFunctionExpression' && arg.body && arg.body.type === 'ObjectExpression')
457
+ obj = arg.body;
458
+ }
459
+ if (!obj && decl.type === 'ObjectExpression')
460
+ obj = decl;
461
+ if (!obj || !Array.isArray(obj.properties))
462
+ continue;
463
+ for (const prop of obj.properties) {
464
+ if (prop.type !== 'Property')
465
+ continue;
466
+ const key = prop.key;
467
+ const keyName = key.type === 'Identifier' ? key.name : key.type === 'Literal' ? key.value : null;
468
+ if (keyName === 'plugins' && prop.value && prop.value.type === 'ArrayExpression') {
469
+ const arr = prop.value;
470
+ const open = arr.start;
471
+ const close = arr.end - 1;
472
+ const objOpen = obj.start;
473
+ const objClose = obj.end - 1;
474
+ // acorn `start` is at `[` and `end` after `]`, but we need indices of brackets
475
+ // verify they indeed point to brackets; adjust if needed
476
+ const realOpen = src.indexOf('[', open);
477
+ const realClose = src.lastIndexOf(']', arr.end - 1);
478
+ return { open: realOpen !== -1 ? realOpen : open, close: realClose !== -1 ? realClose : close, objOpen, objClose };
479
+ }
480
+ }
481
+ if (obj) {
482
+ const objOpen = obj.start;
483
+ const objClose = obj.end - 1;
484
+ // plugins not present but object exists -> caller will insert new property
485
+ // signal with open=-1
486
+ return { open: -1, close: -1, objOpen, objClose };
487
+ }
488
+ }
489
+ }
490
+ }
491
+ // fallback to string scan (no AST or no defineConfig)
492
+ const marker = 'defineConfig(';
493
+ const idx = src.indexOf(marker);
494
+ if (idx === -1)
495
+ return null;
496
+ const objOpen = src.indexOf('{', idx + marker.length);
497
+ if (objOpen === -1)
498
+ return null;
499
+ const objClose = findMatchingBrace(src, objOpen);
500
+ if (objClose === -1)
501
+ return null;
502
+ const objContentStart = objOpen + 1;
503
+ const objContentEnd = objClose;
504
+ let depth = 0;
505
+ let inStr = null;
506
+ let esc = false;
507
+ for (let i = objContentStart; i < objContentEnd;) {
508
+ const c = src[i];
509
+ if (inStr) {
510
+ if (esc) {
511
+ esc = false;
512
+ i++;
513
+ continue;
514
+ }
515
+ if (c === '\\') {
516
+ esc = true;
517
+ i++;
518
+ continue;
519
+ }
520
+ if (c === inStr)
521
+ inStr = null;
522
+ i++;
523
+ continue;
524
+ }
525
+ if (c === '"' || c === "'" || c === '`') {
526
+ inStr = c;
527
+ i++;
528
+ continue;
529
+ }
530
+ if (c === '/' && src[i + 1] === '/') {
531
+ const nl = src.indexOf('\n', i);
532
+ i = nl === -1 ? objContentEnd : nl + 1;
533
+ continue;
534
+ }
535
+ if (c === '/' && src[i + 1] === '*') {
536
+ const end = src.indexOf('*/', i + 2);
537
+ i = end === -1 ? objContentEnd : end + 2;
538
+ continue;
539
+ }
540
+ if (c === '{' || c === '[' || c === '(') {
541
+ depth++;
542
+ i++;
543
+ continue;
544
+ }
545
+ if (c === '}' || c === ']' || c === ')') {
546
+ depth = Math.max(0, depth - 1);
547
+ i++;
548
+ continue;
549
+ }
550
+ if (depth === 0 && src.slice(i, i + 7) === 'plugins') {
551
+ const after = src[i + 7];
552
+ if (after && /[A-Za-z0-9_$]/.test(after)) {
553
+ i += 7;
554
+ continue;
555
+ }
556
+ let j = i + 7;
557
+ while (j < objContentEnd && /\s/.test(src[j]))
558
+ j++;
559
+ if (src[j] !== ':') {
560
+ i = j + 1;
561
+ continue;
562
+ }
563
+ j++;
564
+ while (j < objContentEnd && /\s/.test(src[j]))
565
+ j++;
566
+ if (src[j] !== '[') {
567
+ i = j + 1;
568
+ continue;
569
+ }
570
+ const open = j;
571
+ const close = findMatchingBracket(src, open, '[', ']');
572
+ if (close === -1 || close > objClose)
573
+ return null;
574
+ return { open, close, objOpen, objClose };
575
+ }
576
+ i++;
577
+ }
578
+ return null;
579
+ }
580
+ function insertIntoPluginsArray(source, entryCall) {
581
+ // AST path: check if entry already present via AST
582
+ const ast = parseAstOrNull(source);
583
+ if (ast) {
584
+ for (const node of ast.body) {
585
+ if (node.type === 'ExportDefaultDeclaration' && node.declaration) {
586
+ let decl = node.declaration;
587
+ let obj = null;
588
+ if (decl.type === 'CallExpression' && decl.callee && decl.callee.type === 'Identifier' && decl.callee.name === 'defineConfig' && decl.arguments[0] && decl.arguments[0].type === 'ObjectExpression')
589
+ obj = decl.arguments[0];
590
+ if (!obj && decl.type === 'ObjectExpression')
591
+ obj = decl;
592
+ if (obj) {
593
+ for (const prop of obj.properties) {
594
+ if (prop.type === 'Property') {
595
+ const k = prop.key;
596
+ const kn = k.type === 'Identifier' ? k.name : k.type === 'Literal' ? k.value : null;
597
+ if (kn === 'plugins' && prop.value && prop.value.type === 'ArrayExpression') {
598
+ const arr = prop.value;
599
+ const importName = entryCall.split('(')[0].trim();
600
+ for (const el of arr.elements) {
601
+ if (!el)
602
+ continue;
603
+ const txt = source.slice(el.start, el.end);
604
+ if (txt.includes(importName))
605
+ return source;
606
+ if (el.type === 'Identifier' && el.name === importName)
607
+ return source;
608
+ if (el.type === 'CallExpression' && el.callee && el.callee.type === 'Identifier' && el.callee.name === importName)
609
+ return source;
610
+ }
611
+ const open = arr.start;
612
+ const close = arr.end - 1;
613
+ const inner = source.slice(open + 1, close);
614
+ if (inner.trim() === '') {
615
+ return source.slice(0, open + 1) + '\n\t\t' + entryCall + '\n\t' + source.slice(close);
616
+ }
617
+ const lastEl = arr.elements[arr.elements.length - 1];
618
+ const between = source.slice(lastEl.end, close);
619
+ const needsComma = !between.includes(',');
620
+ const before = source.slice(0, close);
621
+ const after = source.slice(close);
622
+ const sep = needsComma ? ',' : '';
623
+ return before.replace(/\s*$/, '') + sep + '\n\t\t' + entryCall + '\n\t' + after;
624
+ }
625
+ }
626
+ }
627
+ // no plugins property -> insert via AST object bounds
628
+ const objOpen = obj.start;
629
+ const objClose = obj.end - 1;
630
+ const beforeClose = source.slice(0, objClose);
631
+ const afterClose = source.slice(objClose);
632
+ const objInner = source.slice(objOpen + 1, objClose).trim();
633
+ const prefix = objInner ? ',' : '';
634
+ return beforeClose.replace(/\s*$/, '') + prefix + '\n\tplugins: [\n\t\t' + entryCall + '\n\t]\n' + afterClose;
635
+ }
636
+ }
637
+ }
638
+ }
639
+ const bounds = findPluginsArrayBounds(source);
640
+ if (bounds && bounds.open !== -1) {
641
+ const { open, close } = bounds;
642
+ const inner = source.slice(open + 1, close);
643
+ const importName = entryCall.split('(')[0].trim();
644
+ if (importName && inner.includes(importName))
645
+ return source;
646
+ if (inner.trim() === '') {
647
+ return source.slice(0, open + 1) + '\n\t\t' + entryCall + '\n\t' + source.slice(close);
648
+ }
649
+ const trimmedRight = inner.replace(/\s+$/, '');
650
+ const needsComma = !/,\s*$/.test(inner) && trimmedRight.length > 0;
651
+ const before = source.slice(0, close);
652
+ const after = source.slice(close);
653
+ const sep = needsComma ? ',' : '';
654
+ return before.replace(/\s*$/, '') + sep + '\n\t\t' + entryCall + '\n\t' + after;
655
+ }
656
+ if (bounds) {
657
+ const { objOpen, objClose } = bounds;
658
+ const beforeClose = source.slice(0, objClose);
659
+ const afterClose = source.slice(objClose);
660
+ const objInner = source.slice(objOpen + 1, objClose).trim();
661
+ const prefix = objInner ? ',' : '';
662
+ return beforeClose.replace(/\s*$/, '') + prefix + '\n\tplugins: [\n\t\t' + entryCall + '\n\t]\n' + afterClose;
663
+ }
664
+ const marker = 'defineConfig(';
665
+ const idx = source.indexOf(marker);
666
+ if (idx === -1)
667
+ return source;
668
+ const objOpen2 = source.indexOf('{', idx + marker.length);
669
+ if (objOpen2 === -1)
670
+ return source;
671
+ const objClose2 = findMatchingBrace(source, objOpen2);
672
+ if (objClose2 === -1)
673
+ return source;
674
+ const beforeClose2 = source.slice(0, objClose2);
675
+ const afterClose2 = source.slice(objClose2);
676
+ const objInner2 = source.slice(objOpen2 + 1, objClose2).trim();
677
+ const prefix2 = objInner2 ? ',' : '';
678
+ return beforeClose2.replace(/\s*$/, '') + prefix2 + '\n\tplugins: [\n\t\t' + entryCall + '\n\t]\n' + afterClose2;
679
+ }
680
+ function removeFromPluginsArray(source, importName) {
681
+ const ast = parseAstOrNull(source);
682
+ if (ast) {
683
+ for (const node of ast.body) {
684
+ if (node.type === 'ExportDefaultDeclaration' && node.declaration) {
685
+ let decl = node.declaration;
686
+ let obj = null;
687
+ if (decl.type === 'CallExpression' && decl.callee && decl.callee.type === 'Identifier' && decl.callee.name === 'defineConfig' && decl.arguments[0] && decl.arguments[0].type === 'ObjectExpression')
688
+ obj = decl.arguments[0];
689
+ if (!obj && decl.type === 'ObjectExpression')
690
+ obj = decl;
691
+ if (!obj)
692
+ continue;
693
+ for (const prop of obj.properties) {
694
+ if (prop.type !== 'Property')
695
+ continue;
696
+ const k = prop.key;
697
+ const kn = k.type === 'Identifier' ? k.name : k.type === 'Literal' ? k.value : null;
698
+ if (kn === 'plugins' && prop.value && prop.value.type === 'ArrayExpression') {
699
+ const arr = prop.value;
700
+ const kept = [];
701
+ let foundIdx = -1;
702
+ for (let i = 0; i < arr.elements.length; i++) {
703
+ const el = arr.elements[i];
704
+ if (!el)
705
+ continue;
706
+ const txt = source.slice(el.start, el.end);
707
+ const isTarget = txt.includes(importName) || (el.type === 'Identifier' && el.name === importName) || (el.type === 'CallExpression' && el.callee && el.callee.type === 'Identifier' && el.callee.name === importName);
708
+ if (isTarget)
709
+ foundIdx = i;
710
+ else
711
+ kept.push(el);
712
+ }
713
+ if (foundIdx === -1)
714
+ return source;
715
+ const open = arr.start;
716
+ const close = arr.end - 1;
717
+ let newInner;
718
+ if (kept.length === 0)
719
+ newInner = '';
720
+ else
721
+ newInner = '\n\t\t' + kept.map((e) => source.slice(e.start, e.end).trim()).join(',\n\t\t') + '\n\t';
722
+ return source.slice(0, open + 1) + newInner + source.slice(close);
723
+ }
724
+ }
725
+ }
726
+ }
727
+ }
728
+ const bounds = findPluginsArrayBounds(source);
729
+ if (!bounds || bounds.open === -1)
730
+ return source;
731
+ const { open, close } = bounds;
732
+ let inner = source.slice(open + 1, close);
733
+ if (!inner.includes(importName))
734
+ return source;
735
+ const entries = [];
736
+ let lastSplit = 0;
737
+ let depth = 0;
738
+ let inStr = null;
739
+ let esc = false;
740
+ for (let i = 0; i < inner.length; i++) {
741
+ const c = inner[i];
742
+ if (inStr) {
743
+ if (esc) {
744
+ esc = false;
745
+ continue;
746
+ }
747
+ if (c === '\\') {
748
+ esc = true;
749
+ continue;
750
+ }
751
+ if (c === inStr)
752
+ inStr = null;
753
+ continue;
754
+ }
755
+ if (c === '"' || c === "'" || c === '`') {
756
+ inStr = c;
757
+ continue;
758
+ }
759
+ if (c === '/' && inner[i + 1] === '/') {
760
+ const nl = inner.indexOf('\n', i);
761
+ i = nl === -1 ? inner.length : nl;
762
+ continue;
763
+ }
764
+ if (c === '/' && inner[i + 1] === '*') {
765
+ const end = inner.indexOf('*/', i + 2);
766
+ i = end === -1 ? inner.length : end + 1;
767
+ continue;
768
+ }
769
+ if (c === '(' || c === '[' || c === '{')
770
+ depth++;
771
+ else if (c === ')' || c === ']' || c === '}')
772
+ depth = Math.max(0, depth - 1);
773
+ else if (c === ',' && depth === 0) {
774
+ entries.push({ start: lastSplit, end: i, text: inner.slice(lastSplit, i) });
775
+ lastSplit = i + 1;
776
+ }
777
+ }
778
+ entries.push({ start: lastSplit, end: inner.length, text: inner.slice(lastSplit) });
779
+ let targetIdx = -1;
780
+ for (let i = 0; i < entries.length; i++)
781
+ if (entries[i].text.includes(importName)) {
782
+ targetIdx = i;
783
+ break;
784
+ }
785
+ if (targetIdx === -1)
786
+ return source;
787
+ const kept2 = entries.filter((_, idx) => idx !== targetIdx).map(e => e.text).filter(t => t.trim() !== '');
788
+ let newInner2;
789
+ if (kept2.length === 0)
790
+ newInner2 = '';
791
+ else
792
+ newInner2 = '\n\t\t' + kept2.map(t => t.trim()).join(',\n\t\t') + '\n\t';
793
+ return source.slice(0, open + 1) + newInner2 + source.slice(close);
794
+ }
795
+ function hasIdentifierAst(source, name) {
796
+ const ast = parseAstOrNull(source);
797
+ if (!ast)
798
+ return source.includes(name);
799
+ let found = false;
800
+ function walk(node) {
801
+ if (!node || typeof node !== 'object' || found)
802
+ return;
803
+ if (node.type === 'Identifier' && node.name === name) {
804
+ found = true;
805
+ return;
806
+ }
807
+ for (const k of Object.keys(node)) {
808
+ const v = node[k];
809
+ if (Array.isArray(v))
810
+ for (const el of v)
811
+ walk(el);
812
+ else if (v && typeof v.type === 'string')
813
+ walk(v);
814
+ }
815
+ }
816
+ walk(ast);
817
+ return found;
818
+ }
819
+ /**
820
+ * Surgically add a plugin import + `plugins: [...]` entry to `vesk.config.ts`.
821
+ * Idempotent - no duplicate import/entry if already present. Validates via
822
+ * `parseConfigSource` before writing so an invalid file is never clobbered.
823
+ * Uses AST for all syntax analysis (no regex for import/plugins detection).
824
+ */
825
+ export async function addPluginToConfig(projectDir, pkg) {
826
+ const { path, isTs } = findConfigFile(projectDir);
827
+ let target = path;
828
+ let source;
829
+ if (!path) {
830
+ const importName = importNameForPackage(pkg);
831
+ const entry = pkg === '@vesk/plugin-tailwind' ? `${importName}({ entry: 'src/global.css', appDir: 'app' })` : `${importName}()`;
832
+ source = `import { defineConfig } from '@vesk/compiler'\nimport ${importName} from '${pkg}'\n\nexport default defineConfig({\n\tplugins: [\n\t\t${entry}\n\t]\n})\n`;
833
+ target = resolve(projectDir, 'vesk.config.ts');
834
+ await writeConfigSource(projectDir, source);
835
+ return;
836
+ }
837
+ source = readFileSync(path, 'utf-8');
838
+ const existingImport = findPluginImportAst(source, pkg);
839
+ let importName;
840
+ if (existingImport) {
841
+ importName = existingImport.name;
842
+ // check if plugins array already contains it (AST)
843
+ const ast = parseAstOrNull(source);
844
+ if (ast) {
845
+ for (const node of ast.body) {
846
+ if (node.type === 'ExportDefaultDeclaration' && node.declaration) {
847
+ let decl = node.declaration;
848
+ let obj = null;
849
+ if (decl.type === 'CallExpression' && decl.callee && decl.callee.type === 'Identifier' && decl.callee.name === 'defineConfig' && decl.arguments[0] && decl.arguments[0].type === 'ObjectExpression')
850
+ obj = decl.arguments[0];
851
+ if (!obj && decl.type === 'ObjectExpression')
852
+ obj = decl;
853
+ if (obj) {
854
+ for (const prop of obj.properties) {
855
+ if (prop.type === 'Property') {
856
+ const k = prop.key;
857
+ const kn = k.type === 'Identifier' ? k.name : k.type === 'Literal' ? k.value : null;
858
+ if (kn === 'plugins' && prop.value && prop.value.type === 'ArrayExpression') {
859
+ for (const el of prop.value.elements) {
860
+ if (!el)
861
+ continue;
862
+ const txt = source.slice(el.start, el.end);
863
+ if (txt.includes(importName) || (el.type === 'Identifier' && el.name === importName) || (el.type === 'CallExpression' && el.callee && el.callee.type === 'Identifier' && el.callee.name === importName))
864
+ return;
865
+ }
866
+ }
867
+ }
868
+ }
869
+ }
870
+ }
871
+ }
872
+ }
873
+ else {
874
+ const bounds = findPluginsArrayBounds(source);
875
+ if (bounds && bounds.open !== -1) {
876
+ const inner = source.slice(bounds.open + 1, bounds.close);
877
+ if (inner.includes(importName))
878
+ return;
879
+ }
880
+ }
881
+ }
882
+ else {
883
+ importName = importNameForPackage(pkg);
884
+ if (pkg === '@vesk/plugin-tailwind' && importName === 'tailwind')
885
+ importName = 'tailwindcss';
886
+ let base = importName;
887
+ let n = 1;
888
+ while (hasIdentifierAst(source, base)) {
889
+ if (findPluginImportAst(source, pkg))
890
+ break;
891
+ base = `${importName}${n++}`;
892
+ if (n > 20)
893
+ break;
894
+ }
895
+ if (base !== importName)
896
+ importName = base;
897
+ const importLine = `import ${importName} from '${pkg}'\n`;
898
+ const lastImportEnd = findLastImportEnd(source);
899
+ if (lastImportEnd !== -1) {
900
+ source = source.slice(0, lastImportEnd) + importLine + source.slice(lastImportEnd);
901
+ }
902
+ else {
903
+ const expIdx = source.indexOf('export default');
904
+ if (expIdx !== -1)
905
+ source = source.slice(0, expIdx) + importLine + '\n' + source.slice(expIdx);
906
+ else
907
+ source = importLine + source;
908
+ }
909
+ }
910
+ const entry = pkg === '@vesk/plugin-tailwind' ? `${importName}({ entry: 'src/global.css', appDir: 'app' })` : `${importName}()`;
911
+ const newSource = insertIntoPluginsArray(source, entry);
912
+ if (newSource === source)
913
+ return;
914
+ await writeConfigSource(projectDir, newSource);
915
+ }
916
+ /**
917
+ * Surgically remove a plugin's import and its `plugins: [...]` entry.
918
+ * Returns true if file was changed. Uses AST for import/plugins detection.
919
+ */
920
+ export async function removePluginFromConfig(projectDir, pkg) {
921
+ const { path } = findConfigFile(projectDir);
922
+ if (!path)
923
+ return false;
924
+ let source = readFileSync(path, 'utf-8');
925
+ const original = source;
926
+ const existingImport = findPluginImportAst(source, pkg);
927
+ let importName = existingImport ? existingImport.name : null;
928
+ if (!importName)
929
+ importName = importNameForPackage(pkg);
930
+ if (pkg === '@vesk/plugin-tailwind' && !existingImport) {
931
+ const alt = findPluginImportAst(source, '@vesk/plugin-tailwind');
932
+ if (alt)
933
+ importName = alt.name;
934
+ }
935
+ // remove import via AST range
936
+ if (existingImport) {
937
+ source = source.slice(0, existingImport.start) + source.slice(existingImport.end);
938
+ // trim one following newline if present to avoid double blank line
939
+ if (source[existingImport.start] === '\n' && source[existingImport.start - 1] === '\n') {
940
+ // keep single
941
+ }
942
+ }
943
+ else {
944
+ // fallback: no AST node but pkg string present (e.g. comment) - try string replace as last resort
945
+ const fallbackRe = new RegExp(`^[ \\t]*import\\s+\\w+\\s+from\\s+['"]${escapeRegExp(pkg)}['"]\\s*;?\\s*\\n?`, 'm');
946
+ source = source.replace(fallbackRe, '');
947
+ }
948
+ if (importName) {
949
+ const maybeNew = removeFromPluginsArray(source, importName);
950
+ if (maybeNew === source && pkg === '@vesk/plugin-tailwind' && importName === 'tailwind') {
951
+ const alt2 = removeFromPluginsArray(source, 'tailwindcss');
952
+ if (alt2 !== source)
953
+ source = alt2;
954
+ }
955
+ else {
956
+ source = maybeNew;
957
+ }
958
+ }
959
+ source = source.replace(/\n{3,}/g, '\n\n');
960
+ if (source === original)
961
+ return false;
962
+ await writeConfigSource(projectDir, source);
963
+ return true;
964
+ }