@ripast/vue 0.0.0 → 0.1.0

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 +142 -39
  2. package/package.json +3 -3
package/dist/index.mjs CHANGED
@@ -1,8 +1,7 @@
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, join, relative, resolve } from "node:path";
3
+ import { extractTemplateExpressions, hyphenateVueName, isGeneratedNuxtPath, isInsideNuxtAutoImportScope, loadNuxtPathAliases, posToLineCol, resolveBestImportSpecifier, rewriteTemplateReferences, rgFiles } from "@ripast/core/adapter";
4
4
  import { URI } from "vscode-uri";
5
- import { spawnSync } from "node:child_process";
6
5
  import process from "node:process";
7
6
  import { FileType, createLanguage, createLanguageService, createUriMap } from "@volar/language-service";
8
7
  import { createLanguageServiceHost, resolveFileLanguageId } from "@volar/typescript";
@@ -11,6 +10,7 @@ import { createVueLanguageServicePlugins } from "@vue/language-service";
11
10
  import ts from "typescript";
12
11
  import { create } from "volar-service-typescript";
13
12
  import { TextDocument } from "vscode-languageserver-textdocument";
13
+ import { spawnSync } from "node:child_process";
14
14
  function createVueService(tsconfigPath, cwd) {
15
15
  const commandLine = createParsedCommandLine(ts, ts.sys, tsconfigPath);
16
16
  const vueExts = getAllExtensions(commandLine.vueOptions).map((ext) => {
@@ -119,7 +119,7 @@ function workspaceEditToChanges(edit, vue, cwd, filter) {
119
119
  if (after === before) return;
120
120
  out.push({
121
121
  path: fileName,
122
- rel: relPath$1(fileName, cwd),
122
+ rel: workspaceRelativePath(fileName, cwd),
123
123
  before,
124
124
  after
125
125
  });
@@ -130,23 +130,37 @@ function workspaceEditToChanges(edit, vue, cwd, filter) {
130
130
  }
131
131
  return out;
132
132
  }
133
- function relPath$1(fileName, cwd) {
133
+ function workspaceRelativePath(fileName, cwd) {
134
134
  return fileName.startsWith(cwd) ? fileName.slice(cwd.length).replace(/^[/\\]/, "") : fileName;
135
135
  }
136
- function hasVueFilesContaining(cwd, pattern) {
137
- return !!spawnSync("rg", [
136
+ function hasVueFilesContaining$1(cwd, pattern) {
137
+ return listVueFilesContaining(cwd, pattern).length > 0;
138
+ }
139
+ function listVueFiles(cwd) {
140
+ return rgVueFiles(cwd, ["--files"]);
141
+ }
142
+ function listVueFilesContaining(cwd, pattern) {
143
+ return rgVueFiles(cwd, [
138
144
  "--files-with-matches",
145
+ "--fixed-strings",
146
+ pattern
147
+ ]);
148
+ }
149
+ function rgVueFiles(cwd, args) {
150
+ return spawnSync("rg", [
151
+ ...args,
139
152
  "--hidden",
140
153
  "--no-messages",
141
- "--fixed-strings",
142
154
  "-g",
143
155
  "*.vue",
144
- pattern,
145
156
  "."
146
157
  ], {
147
158
  cwd,
148
159
  encoding: "utf8"
149
- }).stdout.trim();
160
+ }).stdout.split("\n").filter(Boolean).map((path) => resolve(cwd, path));
161
+ }
162
+ function hasVueFilesContaining(cwd, pattern) {
163
+ return hasVueFilesContaining$1(cwd, pattern);
150
164
  }
151
165
  async function applyVueRename(tsconfigPath, cwd, from, to, sites) {
152
166
  const vue = createVueService(tsconfigPath, cwd);
@@ -173,11 +187,12 @@ async function applyVueRename(tsconfigPath, cwd, from, to, sites) {
173
187
  const before = existing?.before ?? safeReadFile(path);
174
188
  if (before === void 0) continue;
175
189
  const baseAfter = existing?.after ?? before;
190
+ if (hasLocalScriptBinding(baseAfter, from)) continue;
176
191
  const rewritten = rewriteTemplateReferences(baseAfter, from, to);
177
192
  if (rewritten === baseAfter) continue;
178
193
  byPath.set(path, {
179
194
  path,
180
- rel: existing?.rel ?? relPath(path, cwd),
195
+ rel: existing?.rel ?? workspaceRelativePath(path, cwd),
181
196
  before,
182
197
  after: rewritten
183
198
  });
@@ -187,20 +202,15 @@ async function applyVueRename(tsconfigPath, cwd, from, to, sites) {
187
202
  vue.dispose();
188
203
  }
189
204
  }
190
- function listVueFilesContaining(cwd, pattern) {
191
- return spawnSync("rg", [
192
- "--files-with-matches",
193
- "--hidden",
194
- "--no-messages",
195
- "--fixed-strings",
196
- "-g",
197
- "*.vue",
198
- pattern,
199
- "."
200
- ], {
201
- cwd,
202
- encoding: "utf8"
203
- }).stdout.split("\n").filter(Boolean).map((p) => resolve(cwd, p));
205
+ function hasLocalScriptBinding(source, name) {
206
+ const script = extractScript(source);
207
+ if (!script?.includes(name)) return false;
208
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
209
+ const withoutImports = script.replace(/^\s*import [^;\n]*;?$/gm, "");
210
+ return new RegExp(`\\b(?:const|let|var|function|class|interface|type|enum)\\s+${escaped}\\b`).test(withoutImports);
211
+ }
212
+ function extractScript(source) {
213
+ return /<script(?:\s[^>]*)?>([\s\S]*?)<\/script>/i.exec(source)?.[1] ?? null;
204
214
  }
205
215
  function safeReadFile(path) {
206
216
  try {
@@ -209,9 +219,6 @@ function safeReadFile(path) {
209
219
  return;
210
220
  }
211
221
  }
212
- function relPath(fileName, cwd) {
213
- return fileName.startsWith(cwd) ? fileName.slice(cwd.length).replace(/^[/\\]/, "") : fileName;
214
- }
215
222
  async function applyVueImportRewrite(tsconfigPath, cwd, fromAbs, toAbs) {
216
223
  const vue = createVueService(tsconfigPath, cwd);
217
224
  try {
@@ -266,18 +273,111 @@ async function getDiags(vue, fileName) {
266
273
  function diagKey(d) {
267
274
  return `${d.range.start.line}:${d.range.start.character}:${d.code ?? ""}:${d.message}`;
268
275
  }
269
- function listVueFiles(cwd) {
270
- return spawnSync("rg", [
271
- "--files",
272
- "--hidden",
273
- "--no-messages",
274
- "-g",
275
- "*.vue",
276
- "."
277
- ], {
276
+ async function finalizeVueFileRename(cwd, oldAbs, newAbs, existingChanges, autoImportScopes) {
277
+ const changes = [];
278
+ 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));
287
+ return {
288
+ changes,
289
+ warnings
290
+ };
291
+ }
292
+ function mergeView(a, b) {
293
+ const map = /* @__PURE__ */ new Map();
294
+ for (const c of a) map.set(c.path, c);
295
+ for (const c of b) map.set(c.path, c);
296
+ return [...map.values()];
297
+ }
298
+ function mergeIntoChanges(target, incoming) {
299
+ for (const change of incoming) {
300
+ const existing = target.find((c) => c.path === change.path);
301
+ if (existing) existing.after = change.after;
302
+ else target.push(change);
303
+ }
304
+ }
305
+ function rewriteResolveComponentSites(cwd, oldName, newName, changes, oldAbs, newAbs, warnings) {
306
+ const candidates = new Set(rgFiles("resolveComponent", { cwd }));
307
+ if (!candidates.size) return [];
308
+ const byPath = new Map(changes.map((change) => [change.path, change]));
309
+ const escaped = escapeRe(oldName);
310
+ const literalRe = new RegExp(`resolveComponent\\s*\\(\\s*(['"\`])${escaped}\\1`, "g");
311
+ const out = [];
312
+ const warnFiles = [];
313
+ for (const path of candidates) {
314
+ if (path === oldAbs || path === newAbs || isGeneratedNuxtPath(cwd, path)) continue;
315
+ const before = byPath.get(path)?.before ?? readFileSync(path, "utf8");
316
+ const current = byPath.get(path)?.after ?? before;
317
+ literalRe.lastIndex = 0;
318
+ if (!literalRe.test(current)) continue;
319
+ warnFiles.push(relative(cwd, path));
320
+ if (oldName === newName) continue;
321
+ literalRe.lastIndex = 0;
322
+ const after = current.replace(literalRe, (match, quote) => match.replace(`${quote}${oldName}${quote}`, `${quote}${newName}${quote}`));
323
+ if (after === current) continue;
324
+ out.push({
325
+ path,
326
+ rel: relative(cwd, path),
327
+ before,
328
+ after
329
+ });
330
+ }
331
+ 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).`);
332
+ return out;
333
+ }
334
+ function addExplicitComponentImports(cwd, oldName, newName, newAbs, changes, oldAbs) {
335
+ const tokens = new Set([
336
+ oldName,
337
+ newName,
338
+ hyphenateVueName(oldName),
339
+ hyphenateVueName(newName)
340
+ ]);
341
+ const candidates = /* @__PURE__ */ new Set();
342
+ for (const token of tokens) for (const path of rgFiles(token, {
278
343
  cwd,
279
- encoding: "utf8"
280
- }).stdout.split("\n").filter(Boolean).map((p) => resolve(cwd, p));
344
+ glob: "*.vue"
345
+ })) candidates.add(path);
346
+ if (!candidates.size) return [];
347
+ const aliases = loadNuxtPathAliases(cwd);
348
+ const byPath = new Map(changes.map((change) => [change.path, change]));
349
+ const tagRe = new RegExp(`<\\/?(?:${escapeRe(newName)}|${escapeRe(hyphenateVueName(newName))})(?=[\\s/>])`);
350
+ const out = [];
351
+ for (const path of candidates) {
352
+ if (path === oldAbs || path === newAbs || isGeneratedNuxtPath(cwd, path)) continue;
353
+ const before = byPath.get(path)?.before ?? readFileSync(path, "utf8");
354
+ const current = byPath.get(path)?.after ?? before;
355
+ if (!tagRe.test(current)) continue;
356
+ if (hasDefaultImportFromVue(current, newName)) continue;
357
+ const after = insertVueComponentImport(current, newName, resolveBestImportSpecifier(path, newAbs, aliases, "./placeholder.vue"));
358
+ if (after === current) continue;
359
+ out.push({
360
+ path,
361
+ rel: relative(cwd, path),
362
+ before,
363
+ after
364
+ });
365
+ }
366
+ return out;
367
+ }
368
+ function escapeRe(s) {
369
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
370
+ }
371
+ function hasDefaultImportFromVue(source, name) {
372
+ return new RegExp(`\\bimport\\s+${escapeRe(name)}\\s+from\\s+['"\`][^'"\`]+\\.vue['"\`]`).test(source);
373
+ }
374
+ function insertVueComponentImport(source, name, specifier) {
375
+ const match = source.match(/<script(?:\s[^>]*)?>/);
376
+ 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.`);
377
+ const insertAt = match.index + match[0].length;
378
+ const importLine = `\nimport ${name} from '${specifier}'`;
379
+ if (source.slice(insertAt, insertAt + 1) === "\n") return `${source.slice(0, insertAt)}${importLine}${source.slice(insertAt)}`;
380
+ return `${source.slice(0, insertAt)}${importLine}\n${source.slice(insertAt)}`;
281
381
  }
282
382
  const adapter = {
283
383
  name: "vue",
@@ -298,6 +398,9 @@ const adapter = {
298
398
  autoImportScopes(cwd) {
299
399
  if (!isNuxtProject(cwd)) return /* @__PURE__ */ new Set();
300
400
  return nuxtAutoImportScopes(cwd);
401
+ },
402
+ async finalizeFileRename(cwd, oldAbs, newAbs, existingChanges) {
403
+ return finalizeVueFileRename(cwd, oldAbs, newAbs, existingChanges, isNuxtProject(cwd) ? nuxtAutoImportScopes(cwd) : /* @__PURE__ */ new Set());
301
404
  }
302
405
  };
303
406
  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.0",
4
+ "version": "0.1.0",
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.0"
45
+ "@ripast/core": "0.1.0"
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.0"
58
+ "@ripast/core": "0.1.0"
59
59
  },
60
60
  "scripts": {
61
61
  "build": "obuild",