@ripast/vue 0.0.1 → 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.
Files changed (2) hide show
  1. package/dist/index.mjs +351 -2
  2. 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 { join, resolve } from "node:path";
3
- import { extractTemplateExpressions, posToLineCol, rewriteTemplateReferences } from "@ripast/core/adapter";
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,6 +273,346 @@ 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)$/;
486
+ async function finalizeVueFileRename(cwd, oldAbs, newAbs, existingChanges, autoImportScopes) {
487
+ const changes = [];
488
+ const warnings = [];
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
+ }
512
+ return {
513
+ changes,
514
+ warnings
515
+ };
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
+ }
526
+ function mergeView(a, b) {
527
+ const map = /* @__PURE__ */ new Map();
528
+ for (const c of a) map.set(c.path, c);
529
+ for (const c of b) map.set(c.path, c);
530
+ return [...map.values()];
531
+ }
532
+ function mergeIntoChanges(target, incoming) {
533
+ for (const change of incoming) {
534
+ const existing = target.find((c) => c.path === change.path);
535
+ if (existing) existing.after = change.after;
536
+ else target.push(change);
537
+ }
538
+ }
539
+ function rewriteResolveComponentSites(cwd, oldName, newName, changes, oldAbs, newAbs, warnings) {
540
+ const candidates = new Set(rgFiles("resolveComponent", { cwd }));
541
+ if (!candidates.size) return [];
542
+ const byPath = new Map(changes.map((change) => [change.path, change]));
543
+ const escaped = escapeRe(oldName);
544
+ const literalRe = new RegExp(`resolveComponent\\s*\\(\\s*(['"\`])${escaped}\\1`, "g");
545
+ const out = [];
546
+ const warnFiles = [];
547
+ for (const path of candidates) {
548
+ if (path === oldAbs || path === newAbs || isGeneratedNuxtPath(cwd, path)) continue;
549
+ const before = byPath.get(path)?.before ?? readFileSync(path, "utf8");
550
+ const current = byPath.get(path)?.after ?? before;
551
+ literalRe.lastIndex = 0;
552
+ if (!literalRe.test(current)) continue;
553
+ warnFiles.push(relative(cwd, path));
554
+ if (oldName === newName) continue;
555
+ literalRe.lastIndex = 0;
556
+ const after = current.replace(literalRe, (match, quote) => match.replace(`${quote}${oldName}${quote}`, `${quote}${newName}${quote}`));
557
+ if (after === current) continue;
558
+ out.push({
559
+ path,
560
+ rel: relative(cwd, path),
561
+ before,
562
+ after
563
+ });
564
+ }
565
+ if (warnFiles.length) warnings.push(`rename-file: "${oldName}" is referenced via resolveComponent() in ${warnFiles.length} file(s) [${warnFiles.join(", ")}]. The component must stay globally registered for these sites to resolve (plugin app.component(...) or Nuxt components dir).`);
566
+ return out;
567
+ }
568
+ function addExplicitComponentImports(cwd, oldName, newName, newAbs, changes, oldAbs) {
569
+ const tokens = new Set([
570
+ oldName,
571
+ newName,
572
+ hyphenateVueName(oldName),
573
+ hyphenateVueName(newName)
574
+ ]);
575
+ const candidates = /* @__PURE__ */ new Set();
576
+ for (const token of tokens) for (const path of rgFiles(token, {
577
+ cwd,
578
+ glob: "*.vue"
579
+ })) candidates.add(path);
580
+ if (!candidates.size) return [];
581
+ const aliases = loadNuxtPathAliases(cwd);
582
+ const byPath = new Map(changes.map((change) => [change.path, change]));
583
+ const tagRe = new RegExp(`<\\/?(?:${escapeRe(newName)}|${escapeRe(hyphenateVueName(newName))})(?=[\\s/>])`);
584
+ const out = [];
585
+ for (const path of candidates) {
586
+ if (path === oldAbs || path === newAbs || isGeneratedNuxtPath(cwd, path)) continue;
587
+ const before = byPath.get(path)?.before ?? readFileSync(path, "utf8");
588
+ const current = byPath.get(path)?.after ?? before;
589
+ if (!tagRe.test(current)) continue;
590
+ if (hasDefaultImportFromVue(current, newName)) continue;
591
+ const after = insertVueComponentImport(current, newName, resolveBestImportSpecifier(path, newAbs, aliases, "./placeholder.vue"));
592
+ if (after === current) continue;
593
+ out.push({
594
+ path,
595
+ rel: relative(cwd, path),
596
+ before,
597
+ after
598
+ });
599
+ }
600
+ return out;
601
+ }
602
+ function escapeRe(s) {
603
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
604
+ }
605
+ function hasDefaultImportFromVue(source, name) {
606
+ return new RegExp(`\\bimport\\s+${escapeRe(name)}\\s+from\\s+['"\`][^'"\`]+\\.vue['"\`]`).test(source);
607
+ }
608
+ function insertVueComponentImport(source, name, specifier) {
609
+ const match = source.match(/<script(?:\s[^>]*)?>/);
610
+ if (!match || match.index === void 0) throw new Error(`ripast rename-file: "${name}" is auto-imported in Nuxt; the new path falls outside auto-import scope but a consumer has no <script> block to receive an explicit import. Add a <script setup> block first.`);
611
+ const insertAt = match.index + match[0].length;
612
+ const importLine = `\nimport ${name} from '${specifier}'`;
613
+ if (source.slice(insertAt, insertAt + 1) === "\n") return `${source.slice(0, insertAt)}${importLine}${source.slice(insertAt)}`;
614
+ return `${source.slice(0, insertAt)}${importLine}\n${source.slice(insertAt)}`;
615
+ }
276
616
  const adapter = {
277
617
  name: "vue",
278
618
  hasFilesContaining: hasVueFilesContaining,
@@ -292,6 +632,15 @@ const adapter = {
292
632
  autoImportScopes(cwd) {
293
633
  if (!isNuxtProject(cwd)) return /* @__PURE__ */ new Set();
294
634
  return nuxtAutoImportScopes(cwd);
635
+ },
636
+ isGeneratedPath: isGeneratedNuxtPath,
637
+ filterGeneratedChanges: removeGeneratedNuxtChanges,
638
+ addExplicitImports: (ctx) => addNuxtExplicitImports({
639
+ ...ctx,
640
+ scan
641
+ }),
642
+ async finalizeFileRename(cwd, oldAbs, newAbs, existingChanges) {
643
+ return finalizeVueFileRename(cwd, oldAbs, newAbs, existingChanges, isNuxtProject(cwd) ? nuxtAutoImportScopes(cwd) : /* @__PURE__ */ new Set());
295
644
  }
296
645
  };
297
646
  const DEFAULT_NUXT_AUTO_IMPORT_DIRS = [
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ripast/vue",
3
3
  "type": "module",
4
- "version": "0.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.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.0.1"
58
+ "@ripast/core": "0.2.1"
59
59
  },
60
60
  "scripts": {
61
61
  "build": "obuild",