@ripast/vue 0.1.0 → 0.2.2

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 +250 -10
  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 { basename, join, relative, resolve } from "node:path";
3
- import { extractTemplateExpressions, hyphenateVueName, isGeneratedNuxtPath, isInsideNuxtAutoImportScope, loadNuxtPathAliases, posToLineCol, resolveBestImportSpecifier, rewriteTemplateReferences, rgFiles } 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,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 (!oldAbs.endsWith(".vue") || !newAbs.endsWith(".vue")) return {
280
- changes,
281
- warnings
282
- };
283
- const oldName = basename(oldAbs, ".vue");
284
- const newName = basename(newAbs, ".vue");
285
- mergeIntoChanges(changes, rewriteResolveComponentSites(cwd, oldName, newName, existingChanges, oldAbs, newAbs, warnings));
286
- if (autoImportScopes.size && isInsideNuxtAutoImportScope(oldAbs, autoImportScopes) && !isInsideNuxtAutoImportScope(newAbs, autoImportScopes)) mergeIntoChanges(changes, addExplicitComponentImports(cwd, oldName, newName, newAbs, mergeView(existingChanges, changes), oldAbs));
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.0",
4
+ "version": "0.2.2",
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.0"
45
+ "@ripast/core": "0.2.2"
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.0"
58
+ "@ripast/core": "0.2.2"
59
59
  },
60
60
  "scripts": {
61
61
  "build": "obuild",