@ripast/vue 0.1.0 → 0.2.1
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/index.mjs +250 -10
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
-
import { basename, join, relative, resolve } from "node:path";
|
|
3
|
-
import { extractTemplateExpressions, hyphenateVueName,
|
|
2
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
import { extractTemplateExpressions, hyphenateVueName, isInsideAutoImportScope, posToLineCol, rewriteTemplateReferences, rgFiles, scan } from "@ripast/core/adapter";
|
|
4
4
|
import { URI } from "vscode-uri";
|
|
5
5
|
import process from "node:process";
|
|
6
6
|
import { FileType, createLanguage, createLanguageService, createUriMap } from "@volar/language-service";
|
|
@@ -273,22 +273,256 @@ async function getDiags(vue, fileName) {
|
|
|
273
273
|
function diagKey(d) {
|
|
274
274
|
return `${d.range.start.line}:${d.range.start.character}:${d.code ?? ""}:${d.message}`;
|
|
275
275
|
}
|
|
276
|
+
const MODULE_EXT_RE = /\.(?:tsx?|jsx?|mts|cts|mjs|cjs|vue)$/;
|
|
277
|
+
const WIN_SEP_RE = /\\/g;
|
|
278
|
+
function isGeneratedNuxtPath(cwd, filePath) {
|
|
279
|
+
const rel = relative(cwd, filePath).replace(/\\/g, "/");
|
|
280
|
+
return rel === ".nuxt" || rel.startsWith(".nuxt/");
|
|
281
|
+
}
|
|
282
|
+
function removeGeneratedNuxtChanges(cwd, changes) {
|
|
283
|
+
for (let i = changes.length - 1; i >= 0; i--) if (isGeneratedNuxtPath(cwd, changes[i].path)) changes.splice(i, 1);
|
|
284
|
+
}
|
|
285
|
+
function loadNuxtPathAliases(cwd) {
|
|
286
|
+
const out = [];
|
|
287
|
+
const seen = /* @__PURE__ */ new Set();
|
|
288
|
+
const candidates = [
|
|
289
|
+
join(cwd, ".nuxt/tsconfig.json"),
|
|
290
|
+
join(cwd, ".nuxt/tsconfig.app.json"),
|
|
291
|
+
join(cwd, "tsconfig.json")
|
|
292
|
+
];
|
|
293
|
+
for (const candidate of candidates) {
|
|
294
|
+
if (!existsSync(candidate)) continue;
|
|
295
|
+
for (const alias of readTsconfigPaths(candidate)) {
|
|
296
|
+
const key = `${alias.pattern}|${alias.targets.join(",")}`;
|
|
297
|
+
if (seen.has(key)) continue;
|
|
298
|
+
seen.add(key);
|
|
299
|
+
out.push(alias);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return out;
|
|
303
|
+
}
|
|
304
|
+
function readTsconfigPaths(tsconfigPath) {
|
|
305
|
+
const visited = /* @__PURE__ */ new Set();
|
|
306
|
+
return collect(tsconfigPath);
|
|
307
|
+
function collect(path) {
|
|
308
|
+
if (visited.has(path) || !existsSync(path)) return [];
|
|
309
|
+
visited.add(path);
|
|
310
|
+
let json;
|
|
311
|
+
try {
|
|
312
|
+
json = JSON.parse(stripJsonComments(readFileSync(path, "utf8")));
|
|
313
|
+
} catch {
|
|
314
|
+
return [];
|
|
315
|
+
}
|
|
316
|
+
const out = [];
|
|
317
|
+
const co = json.compilerOptions ?? {};
|
|
318
|
+
const baseUrl = co.baseUrl ? resolve(dirname(path), co.baseUrl) : dirname(path);
|
|
319
|
+
const paths = co.paths ?? {};
|
|
320
|
+
for (const [pattern, raw] of Object.entries(paths)) {
|
|
321
|
+
if (!Array.isArray(raw) || !raw.length) continue;
|
|
322
|
+
const wildcard = pattern.endsWith("/*");
|
|
323
|
+
const targets = raw.map((t) => resolve(baseUrl, wildcard ? t.replace(/\/\*$/, "") : t));
|
|
324
|
+
out.push({
|
|
325
|
+
pattern,
|
|
326
|
+
targets,
|
|
327
|
+
wildcard
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
if (json.extends) {
|
|
331
|
+
const exts = Array.isArray(json.extends) ? json.extends : [json.extends];
|
|
332
|
+
for (const ext of exts) {
|
|
333
|
+
const resolved = ext.startsWith(".") ? resolve(dirname(path), ext) : ext;
|
|
334
|
+
const withExt = /\.json$/.test(resolved) ? resolved : `${resolved}.json`;
|
|
335
|
+
out.push(...collect(withExt));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return out;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function stripJsonComments(source) {
|
|
342
|
+
return source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
|
|
343
|
+
}
|
|
344
|
+
function resolveBestImportSpecifier(fromFile, toFile, aliases, oldSpec) {
|
|
345
|
+
const targetAlias = pickAliasFor(toFile, aliases);
|
|
346
|
+
const sourceAlias = pickAliasFor(fromFile, aliases);
|
|
347
|
+
if (!!targetAlias && (!sourceAlias || sourceAlias.matchedRoot !== targetAlias.matchedRoot) && targetAlias) return formatAliasSpecifier(targetAlias, oldSpec);
|
|
348
|
+
return formatRelativeSpecifier(fromFile, toFile, oldSpec);
|
|
349
|
+
}
|
|
350
|
+
function pickAliasFor(file, aliases) {
|
|
351
|
+
const normFile = file.replace(WIN_SEP_RE, "/");
|
|
352
|
+
let best = null;
|
|
353
|
+
for (const alias of aliases) for (const target of alias.targets) {
|
|
354
|
+
const normTarget = target.replace(WIN_SEP_RE, "/");
|
|
355
|
+
if (alias.wildcard) {
|
|
356
|
+
if (normFile === normTarget || normFile.startsWith(`${normTarget}/`)) {
|
|
357
|
+
const remainder = normFile === normTarget ? "" : normFile.slice(normTarget.length + 1);
|
|
358
|
+
if (!best || normTarget.length > best.matchedRoot.length) best = {
|
|
359
|
+
pattern: alias.pattern,
|
|
360
|
+
matchedRoot: normTarget,
|
|
361
|
+
remainder,
|
|
362
|
+
wildcard: true
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
} else if (normFile === normTarget || stripModuleExt(normFile) === normTarget) {
|
|
366
|
+
if (!best || normTarget.length > best.matchedRoot.length) best = {
|
|
367
|
+
pattern: alias.pattern,
|
|
368
|
+
matchedRoot: normTarget,
|
|
369
|
+
remainder: "",
|
|
370
|
+
wildcard: false
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return best;
|
|
375
|
+
}
|
|
376
|
+
function formatAliasSpecifier(match, oldSpec) {
|
|
377
|
+
if (!match.wildcard) return match.pattern;
|
|
378
|
+
const base = match.pattern.replace(/\/\*$/, "");
|
|
379
|
+
const tail = oldSpec !== void 0 && MODULE_EXT_RE.test(oldSpec) ? match.remainder : match.remainder.replace(MODULE_EXT_RE, "");
|
|
380
|
+
return tail ? `${base}/${tail}` : base;
|
|
381
|
+
}
|
|
382
|
+
function formatRelativeSpecifier(fromFile, toFile, oldSpec) {
|
|
383
|
+
const hasExt = oldSpec !== void 0 && MODULE_EXT_RE.test(oldSpec);
|
|
384
|
+
let rel = relative(dirname(fromFile), toFile).replace(WIN_SEP_RE, "/");
|
|
385
|
+
if (!hasExt) rel = rel.replace(MODULE_EXT_RE, "");
|
|
386
|
+
if (!rel.startsWith(".")) rel = `./${rel}`;
|
|
387
|
+
return rel;
|
|
388
|
+
}
|
|
389
|
+
function stripModuleExt(path) {
|
|
390
|
+
return path.replace(MODULE_EXT_RE, "");
|
|
391
|
+
}
|
|
392
|
+
function addNuxtExplicitImports(ctx) {
|
|
393
|
+
const { cwd, symbols, toAbs, fromAbs, existingChanges, noScriptError, scan } = ctx;
|
|
394
|
+
const byPath = new Map(existingChanges.map((change) => [change.path, change]));
|
|
395
|
+
const aliases = loadNuxtPathAliases(cwd);
|
|
396
|
+
const consumerSymbols = /* @__PURE__ */ new Map();
|
|
397
|
+
for (const symbol of symbols) {
|
|
398
|
+
const hits = scan(symbol, {
|
|
399
|
+
cwd,
|
|
400
|
+
kinds: ["identifier-reference"]
|
|
401
|
+
});
|
|
402
|
+
for (const hit of hits) {
|
|
403
|
+
const abs = resolve(cwd, hit.file);
|
|
404
|
+
if (!consumerSymbols.has(abs)) consumerSymbols.set(abs, /* @__PURE__ */ new Set());
|
|
405
|
+
consumerSymbols.get(abs).add(symbol);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const out = [];
|
|
409
|
+
for (const [filePath, neededSymbols] of consumerSymbols) {
|
|
410
|
+
if (isGeneratedNuxtPath(cwd, filePath)) continue;
|
|
411
|
+
if (filePath === fromAbs || filePath === toAbs) continue;
|
|
412
|
+
let current = byPath.get(filePath)?.after ?? readFileSync(filePath, "utf8");
|
|
413
|
+
const before = byPath.get(filePath)?.before ?? current;
|
|
414
|
+
const specifier = resolveBestImportSpecifier(filePath, toAbs, aliases, "./placeholder");
|
|
415
|
+
let touched = false;
|
|
416
|
+
for (const symbol of neededSymbols) {
|
|
417
|
+
if (hasNamedImport(current, symbol)) continue;
|
|
418
|
+
const next = filePath.endsWith(".vue") ? insertVueScriptImport(current, symbol, specifier, noScriptError) : insertTopLevelImport(current, symbol, specifier);
|
|
419
|
+
if (next !== current) {
|
|
420
|
+
current = next;
|
|
421
|
+
touched = true;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (touched) out.push({
|
|
425
|
+
path: filePath,
|
|
426
|
+
rel: relative(cwd, filePath),
|
|
427
|
+
before,
|
|
428
|
+
after: current
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
return out;
|
|
432
|
+
}
|
|
433
|
+
function insertVueScriptImport(source, symbol, specifier, noScriptError) {
|
|
434
|
+
const match = source.match(/<script(?:\s[^>]*)?>/);
|
|
435
|
+
if (!match || match.index === void 0) throw noScriptError(symbol);
|
|
436
|
+
const insertAt = match.index + match[0].length;
|
|
437
|
+
const scriptEnd = source.indexOf("<\/script>", insertAt);
|
|
438
|
+
const scriptSource = scriptEnd >= 0 ? source.slice(insertAt, scriptEnd) : source.slice(insertAt);
|
|
439
|
+
const mergedScript = mergeNamedImport(scriptSource, symbol, specifier);
|
|
440
|
+
if (mergedScript !== scriptSource) return `${source.slice(0, insertAt)}${mergedScript}${scriptEnd >= 0 ? source.slice(scriptEnd) : ""}`;
|
|
441
|
+
const rest = source[insertAt] === "\n" ? source.slice(insertAt + 1) : source.slice(insertAt);
|
|
442
|
+
return `${source.slice(0, insertAt)}\nimport { ${symbol} } from '${specifier}'\n${rest}`;
|
|
443
|
+
}
|
|
444
|
+
function insertTopLevelImport(source, symbol, specifier) {
|
|
445
|
+
const merged = mergeNamedImport(source, symbol, specifier);
|
|
446
|
+
if (merged !== source) return merged;
|
|
447
|
+
const importLine = `import { ${symbol} } from '${specifier}'\n`;
|
|
448
|
+
if (source.startsWith("#!")) {
|
|
449
|
+
const nl = source.indexOf("\n");
|
|
450
|
+
if (nl >= 0) return `${source.slice(0, nl + 1)}${importLine}${source.slice(nl + 1)}`;
|
|
451
|
+
}
|
|
452
|
+
return `${importLine}${source}`;
|
|
453
|
+
}
|
|
454
|
+
function hasNamedImport(source, symbol) {
|
|
455
|
+
const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
456
|
+
return new RegExp(`\\bimport\\s*\\{[^}]*\\b${escaped}\\b[^}]*\\}\\s*from\\s*['"]`).test(source);
|
|
457
|
+
}
|
|
458
|
+
function mergeNamedImport(source, symbol, specifier) {
|
|
459
|
+
const spec = specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
460
|
+
const match = new RegExp(`import\\s*\\{([^}]*)\\}\\s*from\\s*(['"])${spec}\\2`).exec(source);
|
|
461
|
+
if (!match) return source;
|
|
462
|
+
const names = match[1].split(",").map((part) => part.trim()).filter(Boolean);
|
|
463
|
+
if (names.some((name) => name === symbol || name.startsWith(`${symbol} as `))) return source;
|
|
464
|
+
const replacement = `import { ${[...names, symbol].join(", ")} } from ${match[2]}${specifier}${match[2]}`;
|
|
465
|
+
return `${source.slice(0, match.index)}${replacement}${source.slice(match.index + match[0].length)}`;
|
|
466
|
+
}
|
|
467
|
+
const EXPORT_DECL_RE = /^\s*export\s+(?:async\s+)?(?:function|class|interface|type|enum|const|let|var)\s+([A-Za-z_$][\w$]*)/gm;
|
|
468
|
+
const EXPORT_LIST_RE = /^\s*export\s*\{([^}]+)\}/gm;
|
|
469
|
+
function extractTopLevelExportNames(source) {
|
|
470
|
+
const out = /* @__PURE__ */ new Set();
|
|
471
|
+
for (const match of source.matchAll(EXPORT_DECL_RE)) out.add(match[1]);
|
|
472
|
+
for (const match of source.matchAll(EXPORT_LIST_RE)) for (const part of match[1].split(",")) {
|
|
473
|
+
const cleaned = part.trim();
|
|
474
|
+
if (!cleaned) continue;
|
|
475
|
+
const asMatch = cleaned.match(/^(?:type\s+)?[\w$]+\s+as\s+([A-Za-z_$][\w$]*)$/);
|
|
476
|
+
if (asMatch) {
|
|
477
|
+
out.add(asMatch[1]);
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
const direct = cleaned.replace(/^type\s+/, "").match(/^([A-Z_$][\w$]*)$/i);
|
|
481
|
+
if (direct) out.add(direct[1]);
|
|
482
|
+
}
|
|
483
|
+
return [...out];
|
|
484
|
+
}
|
|
485
|
+
const TS_LIKE_RE = /\.(?:tsx?|mts|cts|jsx?|mjs|cjs)$/;
|
|
276
486
|
async function finalizeVueFileRename(cwd, oldAbs, newAbs, existingChanges, autoImportScopes) {
|
|
277
487
|
const changes = [];
|
|
278
488
|
const warnings = [];
|
|
279
|
-
if (
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
489
|
+
if (oldAbs.endsWith(".vue") && newAbs.endsWith(".vue")) {
|
|
490
|
+
const oldName = basename(oldAbs, ".vue");
|
|
491
|
+
const newName = basename(newAbs, ".vue");
|
|
492
|
+
mergeIntoChanges(changes, rewriteResolveComponentSites(cwd, oldName, newName, existingChanges, oldAbs, newAbs, warnings));
|
|
493
|
+
if (autoImportScopes.size && isInsideAutoImportScope(oldAbs, autoImportScopes) && !isInsideAutoImportScope(newAbs, autoImportScopes)) mergeIntoChanges(changes, addExplicitComponentImports(cwd, oldName, newName, newAbs, mergeView(existingChanges, changes), oldAbs));
|
|
494
|
+
return {
|
|
495
|
+
changes,
|
|
496
|
+
warnings
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
if (TS_LIKE_RE.test(oldAbs) && TS_LIKE_RE.test(newAbs) && autoImportScopes.size && isInsideAutoImportScope(oldAbs, autoImportScopes) && !isInsideAutoImportScope(newAbs, autoImportScopes)) {
|
|
500
|
+
const movedSource = readMovedSource(oldAbs, newAbs, existingChanges);
|
|
501
|
+
const symbols = movedSource ? extractTopLevelExportNames(movedSource) : [];
|
|
502
|
+
if (symbols.length) mergeIntoChanges(changes, addNuxtExplicitImports({
|
|
503
|
+
cwd,
|
|
504
|
+
symbols,
|
|
505
|
+
toAbs: newAbs,
|
|
506
|
+
fromAbs: oldAbs,
|
|
507
|
+
existingChanges: mergeView(existingChanges, changes),
|
|
508
|
+
scan,
|
|
509
|
+
noScriptError: (name) => /* @__PURE__ */ new Error(`ripast rename-file: "${name}" is auto-imported in Nuxt; moving ${basename(oldAbs)} to ${newAbs} takes it out of auto-import scope but a consumer has no <script> block to receive an explicit import. Add a <script setup> block first, or keep the file in composables/utils.`)
|
|
510
|
+
}));
|
|
511
|
+
}
|
|
287
512
|
return {
|
|
288
513
|
changes,
|
|
289
514
|
warnings
|
|
290
515
|
};
|
|
291
516
|
}
|
|
517
|
+
function readMovedSource(oldAbs, newAbs, existingChanges) {
|
|
518
|
+
const self = existingChanges.find((c) => c.path === oldAbs || c.path === newAbs);
|
|
519
|
+
if (self) return self.before;
|
|
520
|
+
try {
|
|
521
|
+
return readFileSync(oldAbs, "utf8");
|
|
522
|
+
} catch {
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
292
526
|
function mergeView(a, b) {
|
|
293
527
|
const map = /* @__PURE__ */ new Map();
|
|
294
528
|
for (const c of a) map.set(c.path, c);
|
|
@@ -399,6 +633,12 @@ const adapter = {
|
|
|
399
633
|
if (!isNuxtProject(cwd)) return /* @__PURE__ */ new Set();
|
|
400
634
|
return nuxtAutoImportScopes(cwd);
|
|
401
635
|
},
|
|
636
|
+
isGeneratedPath: isGeneratedNuxtPath,
|
|
637
|
+
filterGeneratedChanges: removeGeneratedNuxtChanges,
|
|
638
|
+
addExplicitImports: (ctx) => addNuxtExplicitImports({
|
|
639
|
+
...ctx,
|
|
640
|
+
scan
|
|
641
|
+
}),
|
|
402
642
|
async finalizeFileRename(cwd, oldAbs, newAbs, existingChanges) {
|
|
403
643
|
return finalizeVueFileRename(cwd, oldAbs, newAbs, existingChanges, isNuxtProject(cwd) ? nuxtAutoImportScopes(cwd) : /* @__PURE__ */ new Set());
|
|
404
644
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ripast/vue",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"description": "Vue (and Nuxt) framework adapter for ripast. Provides Volar/Vue Language Service-backed rename, move, and template rewriting for .vue SFCs.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"typescript": ">=5.4",
|
|
45
|
-
"@ripast/core": "0.1
|
|
45
|
+
"@ripast/core": "0.2.1"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@volar/language-service": "^2.4.28",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"vscode-uri": "^3.1.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@ripast/core": "0.1
|
|
58
|
+
"@ripast/core": "0.2.1"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"build": "obuild",
|