@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.
- package/dist/client-bundle.d.ts +29 -0
- package/dist/client-bundle.d.ts.map +1 -1
- package/dist/client-bundle.js +333 -52
- package/dist/dev-api.d.ts +78 -0
- package/dist/dev-api.d.ts.map +1 -0
- package/dist/dev-api.js +338 -0
- package/dist/dev-config.d.ts +48 -0
- package/dist/dev-config.d.ts.map +1 -0
- package/dist/dev-config.js +964 -0
- package/dist/dev-server.d.ts +85 -0
- package/dist/dev-server.d.ts.map +1 -1
- package/dist/dev-server.js +329 -8
- package/dist/error-codeframe.d.ts +23 -0
- package/dist/error-codeframe.d.ts.map +1 -0
- package/dist/error-codeframe.js +127 -0
- package/dist/error-tips.d.ts +7 -0
- package/dist/error-tips.d.ts.map +1 -0
- package/dist/error-tips.js +91 -0
- package/dist/hmr-utils.d.ts +14 -0
- package/dist/hmr-utils.d.ts.map +1 -0
- package/dist/hmr-utils.js +56 -0
- package/dist/hmr.d.ts +40 -0
- package/dist/hmr.d.ts.map +1 -1
- package/dist/hmr.js +139 -20
- package/dist/index.d.ts +37 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +105 -22
- package/dist/paths.d.ts +8 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +32 -0
- package/dist/platform-handler.d.ts.map +1 -1
- package/dist/platform-handler.js +2 -1
- package/dist/plugins.d.ts +147 -0
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +1109 -0
- package/dist/prod-server.d.ts.map +1 -1
- package/dist/prod-server.js +43 -9
- package/dist/ssr-function.d.ts.map +1 -1
- package/dist/ssr-function.js +14 -2
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- 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
|
+
}
|