@rsvelte/language-server 0.2.2 → 0.4.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.
package/README.md CHANGED
@@ -18,7 +18,8 @@ toolchain.
18
18
  - **Diagnostics** — push diagnostics from the bundled
19
19
  [`rsvelte_lint`](https://github.com/baseballyama/rsvelte/tree/main/crates/rsvelte_lint)
20
20
  engine (compiled to wasm, vendored in the package — no extra install). Runs on
21
- open, on change (300 ms debounced), and on save.
21
+ open, on change (300 ms debounced), and on save. The rule set comes from the
22
+ project's own [`rsvelte-lint.json`](#lint-configuration).
22
23
 
23
24
  That's the full v1 surface — hover, completion, go-to-definition, rename,
24
25
  find-references, and TypeScript diagnostics are **not** implemented. Those
@@ -37,6 +38,38 @@ The server reads these from the client's `rsvelte.*` configuration:
37
38
  | `rsvelte.lint.enable` | `true` | Enable linting via the bundled engine. |
38
39
  | `rsvelte.rsvelteFmtPath` | `""` | Explicit path to a `rsvelte-fmt` binary (overrides resolution). |
39
40
 
41
+ ## Lint configuration
42
+
43
+ Severities and rule options come from a `rsvelte-lint.json` (or
44
+ `.rsvelte-lintrc.json`), discovered by walking up from the document's directory
45
+ — the same file, in the same order, that the
46
+ [`rsvelte-lint`](https://www.npmjs.com/package/@rsvelte/lint) CLI resolves, so
47
+ the editor reports what CI does. With no config file, every rule runs at its
48
+ default severity (the `recommended` preset); to start from nothing and opt in,
49
+ use `"extends": ["none"]`:
50
+
51
+ ```json
52
+ {
53
+ "extends": ["recommended"],
54
+ "rules": {
55
+ "svelte/no-at-html-tags": "error",
56
+ "svelte/no-unused-class-name": "off",
57
+ "svelte/button-has-type": ["warn", { "submit": true, "reset": false }]
58
+ }
59
+ }
60
+ ```
61
+
62
+ See the
63
+ [CLI's configuration reference](https://www.npmjs.com/package/@rsvelte/lint#configuration)
64
+ for the full shape. An ESLint config is deliberately **not** read: importing it
65
+ is opt-in on the CLI (`--config-from-eslint`), and a server that read it on its
66
+ own would report a different rule set than the same project's CI. A config that
67
+ can't be read or parsed is reported to the client's log and the recommended
68
+ preset is used, so a typo never leaves the editor without diagnostics.
69
+
70
+ Resolved configs are cached for the life of the server; they are dropped when a
71
+ config file is saved through the client, and on `workspace/didChangeConfiguration`.
72
+
40
73
  ## Usage
41
74
 
42
75
  ```sh
@@ -48,6 +81,18 @@ Most users won't run this directly — the
48
81
  bundles and launches it. For other editors, point your LSP client at the
49
82
  `rsvelte-language-server` binary with the `--stdio` transport.
50
83
 
84
+ ## Native server
85
+
86
+ `rsvelte-language-server` is a launcher. It prefers the prebuilt native (Rust)
87
+ server shipped in the optional `@rsvelte/language-server-<platform>` packages
88
+ and falls back to the bundled JS server when no platform package is installed
89
+ (unsupported platform, or an install that skipped optional dependencies).
90
+
91
+ | Environment variable | Effect |
92
+ | --- | --- |
93
+ | `RSVELTE_LANGUAGE_SERVER_BIN` | Run this binary instead of the resolved one. |
94
+ | `RSVELTE_LANGUAGE_SERVER_JS=1` | Force the bundled JS server. |
95
+
51
96
  ## License
52
97
 
53
98
  MIT
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ // Launcher for `rsvelte-language-server`.
3
+ //
4
+ // It prefers the native Rust server shipped in the optional
5
+ // `@rsvelte/language-server-<triple>` packages and falls back to the bundled
6
+ // JS server (`dist/server.mjs`) when no platform package is installed — so an
7
+ // unsupported platform, or an install that skipped optional dependencies,
8
+ // keeps the capabilities it has today instead of failing outright.
9
+ //
10
+ // Like the @rsvelte/fmt and @rsvelte/lint launchers, this file is never
11
+ // rewritten in place: pnpm bakes a shim interpreter from this file's shebang at
12
+ // link time, so swapping it for a native binary post-install would make that
13
+ // shim feed Mach-O/ELF bytes to Node.
14
+
15
+ import { spawnSync } from 'node:child_process';
16
+ import { constants as osConstants } from 'node:os';
17
+ import { existsSync } from 'node:fs';
18
+ import { dirname, join } from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+ import {
21
+ ensureExecutable,
22
+ platformPackage,
23
+ resolvePlatformBinary,
24
+ } from '../lib/resolve.mjs';
25
+
26
+ const pkgRoot = dirname(dirname(fileURLToPath(import.meta.url)));
27
+ const argv = process.argv.slice(2);
28
+
29
+ // Escape hatches for editors and debugging: point at a locally built server, or
30
+ // force the JS implementation when the native one misbehaves.
31
+ const override = process.env.RSVELTE_LANGUAGE_SERVER_BIN;
32
+ const forceJs = process.env.RSVELTE_LANGUAGE_SERVER_JS === '1';
33
+
34
+ let command;
35
+ let args;
36
+ if (override) {
37
+ command = override;
38
+ args = argv;
39
+ } else {
40
+ const native = forceJs ? null : resolvePlatformBinary();
41
+ if (native) {
42
+ ensureExecutable(native);
43
+ command = native;
44
+ args = argv;
45
+ } else {
46
+ const jsServer = join(pkgRoot, 'dist', 'server.mjs');
47
+ if (!existsSync(jsServer)) {
48
+ const { triple, pkgName } = platformPackage();
49
+ const why = forceJs
50
+ ? 'RSVELTE_LANGUAGE_SERVER_JS=1 forced it.'
51
+ : triple
52
+ ? `the native package "${pkgName}" isn't installed.\nTry reinstalling: npm install --include=optional ${pkgName}`
53
+ : `platform ${process.platform}-${process.arch} has no prebuilt binary.`;
54
+ console.error(
55
+ `[@rsvelte/language-server] No server to run: the JS fallback (${jsServer}) is missing and ${why}`,
56
+ );
57
+ process.exit(1);
58
+ }
59
+ command = process.execPath;
60
+ args = [jsServer, ...argv];
61
+ }
62
+ }
63
+
64
+ const result = spawnSync(command, args, { stdio: 'inherit', windowsHide: true });
65
+
66
+ if (result.error) {
67
+ console.error(
68
+ `[@rsvelte/language-server] Failed to exec ${command}: ${result.error.message}`,
69
+ );
70
+ process.exit(1);
71
+ }
72
+
73
+ // A signal termination leaves `status` null; returning `status ?? 0` would mask
74
+ // the crash as a clean exit.
75
+ if (result.signal) {
76
+ const signum = osConstants.signals[result.signal];
77
+ console.error(`[@rsvelte/language-server] ${command} was terminated by ${result.signal}.`);
78
+ process.exit(typeof signum === 'number' ? 128 + signum : 1);
79
+ }
80
+ process.exit(result.status ?? 0);
package/dist/server.mjs CHANGED
@@ -10114,7 +10114,7 @@ function getWellformedEdit(textEdit) {
10114
10114
 
10115
10115
  // src/server.ts
10116
10116
  import { fileURLToPath as fileURLToPath2 } from "node:url";
10117
- import { dirname as dirname3 } from "node:path";
10117
+ import { dirname as dirname4 } from "node:path";
10118
10118
 
10119
10119
  // src/diagnostics.ts
10120
10120
  var import_node = __toESM(require_main3(), 1);
@@ -10233,11 +10233,11 @@ function load() {
10233
10233
  return null;
10234
10234
  }
10235
10235
  }
10236
- function runLint(source, filename) {
10236
+ function runLint(source, filename, config) {
10237
10237
  const m = load();
10238
10238
  if (!m) return null;
10239
10239
  try {
10240
- return m.lint(source, filename);
10240
+ return m.lint_with_config(source, filename, config);
10241
10241
  } catch {
10242
10242
  return null;
10243
10243
  }
@@ -10252,6 +10252,62 @@ function lintVersion() {
10252
10252
  }
10253
10253
  }
10254
10254
 
10255
+ // src/lintConfig.ts
10256
+ import { basename, dirname as dirname3, join as join3 } from "node:path";
10257
+ import { readFileSync } from "node:fs";
10258
+ var CONFIG_NAMES = ["rsvelte-lint.json", ".rsvelte-lintrc.json"];
10259
+ var NO_CONFIG = { text: "", path: null };
10260
+ var cache = /* @__PURE__ */ new Map();
10261
+ function isLintConfigPath(path) {
10262
+ return CONFIG_NAMES.includes(basename(path));
10263
+ }
10264
+ function clearLintConfigCache() {
10265
+ cache.clear();
10266
+ }
10267
+ function resolveLintConfig(dir, onError) {
10268
+ const cached = cache.get(dir);
10269
+ if (cached) return cached;
10270
+ const found = discover(dir);
10271
+ cache.set(dir, found.config);
10272
+ if (found.error) onError?.(`${found.config.path}: ${found.error}`);
10273
+ return found.config;
10274
+ }
10275
+ function discover(start) {
10276
+ let dir = start;
10277
+ for (; ; ) {
10278
+ for (const name of CONFIG_NAMES) {
10279
+ const found = read(join3(dir, name));
10280
+ if (found) return found;
10281
+ }
10282
+ const parent = dirname3(dir);
10283
+ if (parent === dir) return { config: NO_CONFIG, error: null };
10284
+ dir = parent;
10285
+ }
10286
+ }
10287
+ function read(path) {
10288
+ let text;
10289
+ try {
10290
+ text = readFileSync(path, "utf8");
10291
+ } catch (err) {
10292
+ const code = err.code;
10293
+ if (code === "ENOENT" || code === "ENOTDIR") return null;
10294
+ return { config: { text: "", path }, error: String(err) };
10295
+ }
10296
+ let parsed;
10297
+ try {
10298
+ parsed = JSON.parse(text);
10299
+ } catch (err) {
10300
+ return { config: { text: "", path }, error: String(err) };
10301
+ }
10302
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
10303
+ return {
10304
+ config: { text: "", path },
10305
+ error: "lint config must be a JSON object"
10306
+ };
10307
+ }
10308
+ return { config: { text, path }, error: null };
10309
+ }
10310
+
10255
10311
  // src/server.ts
10256
10312
  var DEFAULT_SETTINGS = {
10257
10313
  format: { enable: true },
@@ -10313,9 +10369,8 @@ async function refreshSettings() {
10313
10369
  }
10314
10370
  connection.onDidChangeConfiguration(async () => {
10315
10371
  await refreshSettings();
10316
- for (const doc of documents.all()) {
10317
- scheduleLint(doc, 0);
10318
- }
10372
+ clearLintConfigCache();
10373
+ relintOpenDocuments();
10319
10374
  });
10320
10375
  connection.onDocumentFormatting(async (params) => {
10321
10376
  if (!settings.format.enable) return [];
@@ -10323,7 +10378,7 @@ connection.onDocumentFormatting(async (params) => {
10323
10378
  if (!doc) return [];
10324
10379
  const fsPath = docFsPath(doc.uri);
10325
10380
  const binPath = resolveRsvelteFmt(
10326
- dirname3(fsPath),
10381
+ dirname4(fsPath),
10327
10382
  settings.rsvelteFmtPath
10328
10383
  );
10329
10384
  if (!binPath) return [];
@@ -10350,13 +10405,30 @@ async function lintDocument(doc) {
10350
10405
  connection.sendDiagnostics({ uri: doc.uri, diagnostics: [] });
10351
10406
  return;
10352
10407
  }
10353
- const json = runLint(doc.getText(), docFsPath(doc.uri));
10408
+ const fsPath = docFsPath(doc.uri);
10409
+ const config = resolveLintConfig(
10410
+ dirname4(fsPath),
10411
+ (message) => connection.console.warn(message)
10412
+ );
10413
+ const json = runLint(doc.getText(), fsPath, config.text);
10354
10414
  const diagnostics = json ? lintJsonToDiagnostics(json) : [];
10355
10415
  connection.sendDiagnostics({ uri: doc.uri, diagnostics });
10356
10416
  }
10417
+ function relintOpenDocuments() {
10418
+ for (const doc of documents.all()) {
10419
+ scheduleLint(doc, 0);
10420
+ }
10421
+ }
10357
10422
  documents.onDidOpen((e) => scheduleLint(e.document, 0));
10358
10423
  documents.onDidChangeContent((e) => scheduleLint(e.document));
10359
- documents.onDidSave((e) => scheduleLint(e.document, 0));
10424
+ documents.onDidSave((e) => {
10425
+ if (isLintConfigPath(docFsPath(e.document.uri))) {
10426
+ clearLintConfigCache();
10427
+ relintOpenDocuments();
10428
+ return;
10429
+ }
10430
+ scheduleLint(e.document, 0);
10431
+ });
10360
10432
  documents.onDidClose((e) => {
10361
10433
  const timer = debounceTimers.get(e.document.uri);
10362
10434
  if (timer) {
@@ -269,6 +269,35 @@ function lint_version() {
269
269
  }
270
270
  exports.lint_version = lint_version;
271
271
 
272
+ /**
273
+ * [`lint`] under the text of a `rsvelte-lint.json`. Wasm has no filesystem, so
274
+ * the host (the language server) discovers the config file and passes its
275
+ * contents in; `""` selects the recommended preset.
276
+ * @param {string} source
277
+ * @param {string} filename
278
+ * @param {string} config
279
+ * @returns {string}
280
+ */
281
+ function lint_with_config(source, filename, config) {
282
+ let deferred4_0;
283
+ let deferred4_1;
284
+ try {
285
+ const ptr0 = passStringToWasm0(source, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
286
+ const len0 = WASM_VECTOR_LEN;
287
+ const ptr1 = passStringToWasm0(filename, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
288
+ const len1 = WASM_VECTOR_LEN;
289
+ const ptr2 = passStringToWasm0(config, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
290
+ const len2 = WASM_VECTOR_LEN;
291
+ const ret = wasm.lint_with_config(ptr0, len0, ptr1, len1, ptr2, len2);
292
+ deferred4_0 = ret[0];
293
+ deferred4_1 = ret[1];
294
+ return getStringFromWasm0(ret[0], ret[1]);
295
+ } finally {
296
+ wasm.__wbindgen_free(deferred4_0, deferred4_1, 1);
297
+ }
298
+ }
299
+ exports.lint_with_config = lint_with_config;
300
+
272
301
  /**
273
302
  * Parse a Svelte component and return the AST as JSON.
274
303
  * @param {string} source
@@ -403,8 +432,8 @@ function __wbg_get_imports() {
403
432
  getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
404
433
  },
405
434
  __wbindgen_cast_0000000000000001: function(arg0, arg1) {
406
- // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [String], shim_idx: 164, ret: String, inner_ret: Some(String) }, mutable: false }) -> Externref`.
407
- const ret = makeClosure(arg0, arg1, wasm_bindgen_b67232e9c19c9ca6___convert__closures_____invoke___alloc_a941a2cfab7efbc4___string__String__alloc_a941a2cfab7efbc4___string__String__true_);
435
+ // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [String], shim_idx: 5, ret: String, inner_ret: Some(String) }, mutable: false }) -> Externref`.
436
+ const ret = makeClosure(arg0, arg1, wasm_bindgen_de7100a3341595c3___convert__closures_____invoke___alloc_a941a2cfab7efbc4___string__String__alloc_a941a2cfab7efbc4___string__String__true_);
408
437
  return ret;
409
438
  },
410
439
  __wbindgen_cast_0000000000000002: function(arg0, arg1) {
@@ -428,13 +457,13 @@ function __wbg_get_imports() {
428
457
  };
429
458
  }
430
459
 
431
- function wasm_bindgen_b67232e9c19c9ca6___convert__closures_____invoke___alloc_a941a2cfab7efbc4___string__String__alloc_a941a2cfab7efbc4___string__String__true_(arg0, arg1, arg2) {
460
+ function wasm_bindgen_de7100a3341595c3___convert__closures_____invoke___alloc_a941a2cfab7efbc4___string__String__alloc_a941a2cfab7efbc4___string__String__true_(arg0, arg1, arg2) {
432
461
  let deferred2_0;
433
462
  let deferred2_1;
434
463
  try {
435
464
  const ptr0 = passStringToWasm0(arg2, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
436
465
  const len0 = WASM_VECTOR_LEN;
437
- const ret = wasm.wasm_bindgen_b67232e9c19c9ca6___convert__closures_____invoke___alloc_a941a2cfab7efbc4___string__String__alloc_a941a2cfab7efbc4___string__String__true_(arg0, arg1, ptr0, len0);
466
+ const ret = wasm.wasm_bindgen_de7100a3341595c3___convert__closures_____invoke___alloc_a941a2cfab7efbc4___string__String__alloc_a941a2cfab7efbc4___string__String__true_(arg0, arg1, ptr0, len0);
438
467
  deferred2_0 = ret[0];
439
468
  deferred2_1 = ret[1];
440
469
  return getStringFromWasm0(ret[0], ret[1]);
Binary file
@@ -0,0 +1,74 @@
1
+ // Resolution helpers for the @rsvelte/language-server launcher: they find the
2
+ // platform-native `rsvelte-language-server` binary shipped in the optional
3
+ // `@rsvelte/language-server-<triple>` packages.
4
+ //
5
+ // ESM (not `.cjs` like the @rsvelte/fmt / @rsvelte/lint siblings) because this
6
+ // package is `"type": "module"`; the resolution logic itself is identical.
7
+
8
+ import { chmodSync, constants, statSync } from 'node:fs';
9
+ import { createRequire } from 'node:module';
10
+
11
+ const require = createRequire(import.meta.url);
12
+
13
+ /// Map the current platform/arch to a `@rsvelte/language-server-<triple>`
14
+ /// suffix, or `null` when unsupported. Mirrors the release build matrix.
15
+ export function resolveTriple() {
16
+ const { platform, arch } = process;
17
+ if (platform === 'darwin') {
18
+ if (arch === 'arm64') return 'darwin-arm64';
19
+ if (arch === 'x64') return 'darwin-x64';
20
+ } else if (platform === 'linux') {
21
+ // Node 18+ exposes the runtime glibc version; an empty value means musl.
22
+ let isMusl = false;
23
+ try {
24
+ const header = process.report.getReport().header;
25
+ isMusl = !header.glibcVersionRuntime;
26
+ } catch {
27
+ isMusl = false;
28
+ }
29
+ const libc = isMusl ? 'musl' : 'gnu';
30
+ if (arch === 'x64') return `linux-x64-${libc}`;
31
+ if (arch === 'arm64') return `linux-arm64-${libc}`;
32
+ } else if (platform === 'win32') {
33
+ if (arch === 'x64') return 'win32-x64-msvc';
34
+ }
35
+ return null;
36
+ }
37
+
38
+ /// The platform package name + binary basename for the current platform.
39
+ /// `triple` is `null` when the platform is unsupported.
40
+ export function platformPackage() {
41
+ const triple = resolveTriple();
42
+ if (!triple) return { triple: null, pkgName: null, binName: null };
43
+ const pkgName = `@rsvelte/language-server-${triple}`;
44
+ const binName =
45
+ process.platform === 'win32'
46
+ ? 'rsvelte-language-server.exe'
47
+ : 'rsvelte-language-server';
48
+ return { triple, pkgName, binName };
49
+ }
50
+
51
+ /// Resolve the absolute path to the prebuilt native server for this platform,
52
+ /// or `null` when the optional platform package isn't installed.
53
+ export function resolvePlatformBinary() {
54
+ const { pkgName, binName } = platformPackage();
55
+ if (!pkgName) return null;
56
+ try {
57
+ return require.resolve(`${pkgName}/${binName}`);
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ /// Best-effort `chmod +x` on a POSIX file. No-op on Windows / read-only FS.
64
+ export function ensureExecutable(binPath) {
65
+ if (process.platform === 'win32') return;
66
+ try {
67
+ const mode = statSync(binPath).mode;
68
+ if (!(mode & constants.S_IXUSR)) {
69
+ chmodSync(binPath, (mode & 0o777) | 0o111);
70
+ }
71
+ } catch {
72
+ // Not fatal — a later spawn surfaces a clear error if it really can't run.
73
+ }
74
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rsvelte/language-server",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
4
4
  "description": "Language Server for rsvelte — formatting (native rsvelte-fmt) and linting (bundled rsvelte_lint wasm)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -22,13 +22,22 @@
22
22
  ],
23
23
  "type": "module",
24
24
  "bin": {
25
- "rsvelte-language-server": "dist/server.mjs"
25
+ "rsvelte-language-server": "bin/rsvelte-language-server.mjs"
26
26
  },
27
27
  "files": [
28
+ "bin/rsvelte-language-server.mjs",
29
+ "lib/resolve.mjs",
28
30
  "dist/server.mjs",
29
31
  "dist/vendor/rsvelte_lint.cjs",
30
32
  "dist/vendor/rsvelte_lint_bg.wasm"
31
33
  ],
34
+ "optionalDependencies": {
35
+ "@rsvelte/language-server-darwin-arm64": "^0.4.0",
36
+ "@rsvelte/language-server-darwin-x64": "^0.4.0",
37
+ "@rsvelte/language-server-linux-arm64-gnu": "^0.4.0",
38
+ "@rsvelte/language-server-linux-x64-gnu": "^0.4.0",
39
+ "@rsvelte/language-server-win32-x64-msvc": "^0.4.0"
40
+ },
32
41
  "dependencies": {
33
42
  "vscode-languageserver": "^10.0.0",
34
43
  "vscode-languageserver-textdocument": "^1.0.12"