@rsvelte/compiler 0.10.20 → 0.11.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/README.md CHANGED
@@ -101,6 +101,43 @@ The exported names and JSON-string return values below are specific to this
101
101
  WASM package. They are deliberately not presented as `svelte/compiler`
102
102
  equivalents.
103
103
 
104
+ ## Compare CLI
105
+
106
+ Projects that already have Svelte installed can compare official and rsvelte
107
+ output before switching compilers:
108
+
109
+ ```bash
110
+ npx --package @rsvelte/compiler rsvelte compare 'src/**/*.svelte'
111
+ ```
112
+
113
+ The command compiles each component with both compilers and strictly compares
114
+ the generated `js.code` and `css.code`. Source maps are not included: their
115
+ segment layouts can differ even when the shipped code is identical. It exits 0
116
+ only when every file matches; a difference or compile failure exits 1.
117
+
118
+ ```text
119
+ .................X.................
120
+
121
+ 35 files scanned, 34 match, 1 difference
122
+
123
+ Differences:
124
+ * src/components/menu.svelte
125
+ [client js.code] line 8, column 14
126
+ official: "..."
127
+ rsvelte: "..."
128
+ ```
129
+
130
+ Client production output is the default. Use `--generate server` or
131
+ `--generate both` for SSR, `--dev` for development output, and
132
+ `--options compiler-options.json` to merge JSON-compatible Svelte compiler
133
+ options. The CLI sets `filename` and `generate` per comparison. Directories and
134
+ quoted `*`, `**`, and `?` globs are supported; `node_modules`, `.git`, and
135
+ `target` are skipped during directory walks.
136
+
137
+ This is a generated-output check, not an application test: preprocessors, Vite
138
+ plugin behavior, runtime-version differences, source maps, and browser/server
139
+ behavior remain outside its comparison key.
140
+
104
141
  ## Why it is fast
105
142
 
106
143
  - Written in Rust with a memory-efficient AST (`u32` spans, compact strings) and
@@ -121,6 +158,7 @@ MIT
121
158
  ---
122
159
 
123
160
  > **Maintainers:** `@rsvelte/compiler` is published from the wasm-pack output in
124
- > the repo-root `pkg/` directory, not from this directory. See
161
+ > the repo-root `pkg/` directory. `finalize-pkg.mjs` overlays this README and the
162
+ > hand-written `bin/` + `lib/` files from this directory. See
125
163
  > [`PUBLISHING.md`](./PUBLISHING.md) for how the version anchor and README
126
164
  > overlay work.
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from 'node:fs';
4
+ import { createRequire } from 'node:module';
5
+ import { resolve } from 'node:path';
6
+ import { pathToFileURL } from 'node:url';
7
+
8
+ import {
9
+ HELP,
10
+ UsageError,
11
+ buildReport,
12
+ collectSvelteFiles,
13
+ compareFiles,
14
+ formatHumanReport,
15
+ parseArgs,
16
+ readCompilerOptions,
17
+ } from '../lib/compare.mjs';
18
+
19
+ async function main() {
20
+ if (process.argv[2] !== 'compare') {
21
+ if (process.argv[2] === '-h' || process.argv[2] === '--help') {
22
+ process.stdout.write(`Usage: rsvelte <command>\n\nCommands:\n compare compare official and rsvelte compiler output\n`);
23
+ return 0;
24
+ }
25
+ throw new UsageError(process.argv[2] ? `Unknown command: ${process.argv[2]}` : 'Missing command');
26
+ }
27
+
28
+ const args = parseArgs(process.argv.slice(3));
29
+ if (args.help) {
30
+ process.stdout.write(`${HELP}\n`);
31
+ return 0;
32
+ }
33
+ const files = await collectSvelteFiles(args.paths, process.cwd());
34
+ if (files.length === 0) throw new UsageError('No .svelte files matched');
35
+ const compilerOptions = await readCompilerOptions(args.optionsPath, process.cwd());
36
+ if (args.dev) compilerOptions.dev = true;
37
+ const targets = args.generate === 'both' ? ['client', 'server'] : [args.generate];
38
+ const { officialCompile, rsvelteCompile } = await loadCompilers();
39
+ const results = await compareFiles({
40
+ files,
41
+ cwd: process.cwd(),
42
+ targets,
43
+ options: compilerOptions,
44
+ officialCompile,
45
+ rsvelteCompile,
46
+ });
47
+ const report = buildReport(results);
48
+ process.stdout.write(args.json ? `${JSON.stringify(report, null, 2)}\n` : formatHumanReport(report));
49
+ return report.different === 0 ? 0 : 1;
50
+ }
51
+
52
+ async function loadCompilers() {
53
+ let official;
54
+ try {
55
+ const projectRequire = createRequire(resolve(process.cwd(), 'package.json'));
56
+ official = await import(pathToFileURL(projectRequire.resolve('svelte/compiler')).href);
57
+ } catch {
58
+ throw new UsageError('Cannot load svelte/compiler; install Svelte in the project being compared');
59
+ }
60
+
61
+ const rsvelte = await import('@rsvelte/compiler');
62
+ const require = createRequire(import.meta.url);
63
+ const wasmPath = require.resolve('@rsvelte/compiler/wasm');
64
+ rsvelte.initSync({ module: readFileSync(wasmPath) });
65
+ const officialCompile = official.compile ?? official.default?.compile;
66
+ if (typeof officialCompile !== 'function') {
67
+ throw new UsageError('Loaded svelte/compiler does not export compile()');
68
+ }
69
+ return { officialCompile, rsvelteCompile: rsvelte.compile };
70
+ }
71
+
72
+ try {
73
+ process.exitCode = await main();
74
+ } catch (error) {
75
+ if (error instanceof UsageError) {
76
+ process.stderr.write(`rsvelte: ${error.message}\n\n${HELP}\n`);
77
+ process.exitCode = 2;
78
+ } else {
79
+ process.stderr.write(`rsvelte: ${error?.stack ?? error}\n`);
80
+ process.exitCode = 2;
81
+ }
82
+ }
@@ -0,0 +1,272 @@
1
+ import { lstat, readFile, readdir } from 'node:fs/promises';
2
+ import { isAbsolute, relative, resolve, sep } from 'node:path';
3
+
4
+ const SKIPPED_DIRECTORIES = new Set(['.git', 'node_modules', 'target']);
5
+ const MAGIC = /[*?]/;
6
+ const REGEXP_SPECIAL = new Set('\\^$+?.()|{}[]');
7
+
8
+ export const HELP = `Usage: rsvelte compare [OPTIONS] [PATH...]
9
+
10
+ Compare official Svelte and rsvelte generated JavaScript/CSS byte-for-byte.
11
+
12
+ Arguments:
13
+ PATH .svelte file, directory, or glob (default: .)
14
+
15
+ Options:
16
+ --generate <target> client, server, or both (default: client)
17
+ --dev compile both sides with dev: true
18
+ --options <file> merge JSON compiler options (filename/generate are set per file)
19
+ --json print a machine-readable report
20
+ -h, --help show this help
21
+
22
+ Exit codes: 0 all match, 1 differences or compile failures, 2 usage/setup error.`;
23
+
24
+ export function parseArgs(argv) {
25
+ const parsed = {
26
+ dev: false,
27
+ generate: 'client',
28
+ help: false,
29
+ json: false,
30
+ optionsPath: null,
31
+ paths: [],
32
+ };
33
+
34
+ for (let index = 0; index < argv.length; index += 1) {
35
+ const arg = argv[index];
36
+ if (arg === '--') {
37
+ parsed.paths.push(...argv.slice(index + 1));
38
+ break;
39
+ }
40
+ if (arg === '-h' || arg === '--help') parsed.help = true;
41
+ else if (arg === '--json') parsed.json = true;
42
+ else if (arg === '--dev') parsed.dev = true;
43
+ else if (arg === '--generate') parsed.generate = takeValue(argv, ++index, arg);
44
+ else if (arg.startsWith('--generate=')) parsed.generate = arg.slice('--generate='.length);
45
+ else if (arg === '--options') parsed.optionsPath = takeValue(argv, ++index, arg);
46
+ else if (arg.startsWith('--options=')) parsed.optionsPath = arg.slice('--options='.length);
47
+ else if (arg.startsWith('-')) throw new UsageError(`Unknown option: ${arg}`);
48
+ else parsed.paths.push(arg);
49
+ }
50
+
51
+ if (!['client', 'server', 'both'].includes(parsed.generate)) {
52
+ throw new UsageError(`Invalid --generate value: ${parsed.generate}`);
53
+ }
54
+ if (parsed.paths.length === 0) parsed.paths.push('.');
55
+ return parsed;
56
+ }
57
+
58
+ function takeValue(argv, index, option) {
59
+ const value = argv[index];
60
+ if (!value || value.startsWith('-')) throw new UsageError(`${option} requires a value`);
61
+ return value;
62
+ }
63
+
64
+ export class UsageError extends Error {}
65
+
66
+ export async function collectSvelteFiles(inputs, cwd = process.cwd()) {
67
+ const found = new Set();
68
+ for (const input of inputs) {
69
+ if (MAGIC.test(input)) {
70
+ await collectGlob(input, cwd, found);
71
+ continue;
72
+ }
73
+
74
+ const path = resolve(cwd, input);
75
+ let stat;
76
+ try {
77
+ stat = await lstat(path);
78
+ } catch (error) {
79
+ if (error?.code === 'ENOENT') throw new UsageError(`Path does not exist: ${input}`);
80
+ throw error;
81
+ }
82
+ if (stat.isDirectory()) await walk(path, found);
83
+ else if (stat.isFile() && path.endsWith('.svelte')) found.add(path);
84
+ else throw new UsageError(`Not a .svelte file or directory: ${input}`);
85
+ }
86
+ return [...found].sort((a, b) => a.localeCompare(b));
87
+ }
88
+
89
+ async function collectGlob(pattern, cwd, found) {
90
+ const absolutePattern = slash(isAbsolute(pattern) ? pattern : resolve(cwd, pattern));
91
+ const firstMagic = absolutePattern.search(MAGIC);
92
+ const slashBeforeMagic = absolutePattern.lastIndexOf('/', firstMagic);
93
+ const root = slashBeforeMagic <= 0 ? '/' : absolutePattern.slice(0, slashBeforeMagic);
94
+ const matcher = globRegExp(absolutePattern);
95
+ let stat;
96
+ try {
97
+ stat = await lstat(root);
98
+ } catch (error) {
99
+ if (error?.code === 'ENOENT') return;
100
+ throw error;
101
+ }
102
+ if (!stat.isDirectory()) return;
103
+ await walk(root, found, (path) => matcher.test(slash(path)));
104
+ }
105
+
106
+ async function walk(directory, found, accept = (path) => path.endsWith('.svelte')) {
107
+ const entries = await readdir(directory, { withFileTypes: true });
108
+ for (const entry of entries) {
109
+ const path = resolve(directory, entry.name);
110
+ if (entry.isDirectory()) {
111
+ if (!SKIPPED_DIRECTORIES.has(entry.name)) await walk(path, found, accept);
112
+ } else if (entry.isFile() && path.endsWith('.svelte') && accept(path)) {
113
+ found.add(path);
114
+ }
115
+ }
116
+ }
117
+
118
+ function globRegExp(pattern) {
119
+ let expression = '^';
120
+ for (let index = 0; index < pattern.length; index += 1) {
121
+ const char = pattern[index];
122
+ if (char === '*') {
123
+ if (pattern[index + 1] === '*') {
124
+ index += 1;
125
+ if (pattern[index + 1] === '/') {
126
+ index += 1;
127
+ expression += '(?:.*/)?';
128
+ } else expression += '.*';
129
+ } else expression += '[^/]*';
130
+ } else if (char === '?') expression += '[^/]';
131
+ else expression += REGEXP_SPECIAL.has(char) ? `\\${char}` : char;
132
+ }
133
+ return new RegExp(`${expression}$`);
134
+ }
135
+
136
+ const slash = (path) => path.split(sep).join('/');
137
+
138
+ export async function readCompilerOptions(path, cwd = process.cwd()) {
139
+ if (!path) return {};
140
+ let value;
141
+ try {
142
+ value = JSON.parse(await readFile(resolve(cwd, path), 'utf8'));
143
+ } catch (error) {
144
+ throw new UsageError(`Cannot read compiler options from ${path}: ${error.message}`);
145
+ }
146
+ if (!value || Array.isArray(value) || typeof value !== 'object') {
147
+ throw new UsageError(`Compiler options in ${path} must be a JSON object`);
148
+ }
149
+ delete value.filename;
150
+ delete value.generate;
151
+ return value;
152
+ }
153
+
154
+ export async function compareFiles({ files, cwd, targets, options, officialCompile, rsvelteCompile }) {
155
+ const results = [];
156
+ for (const file of files) {
157
+ const source = await readFile(file, 'utf8');
158
+ const displayPath = slash(relative(cwd, file)) || slash(file);
159
+ const comparisons = targets.map((target) =>
160
+ compareSource({
161
+ source,
162
+ filename: displayPath,
163
+ target,
164
+ options,
165
+ officialCompile,
166
+ rsvelteCompile,
167
+ }),
168
+ );
169
+ results.push({ file: displayPath, match: comparisons.every((item) => item.match), comparisons });
170
+ }
171
+ return results;
172
+ }
173
+
174
+ export function compareSource({ source, filename, target, options, officialCompile, rsvelteCompile }) {
175
+ const compileOptions = { ...options, filename, generate: target };
176
+ let official;
177
+ let actual;
178
+ try {
179
+ official = normalizeResult(officialCompile(source, compileOptions));
180
+ } catch (error) {
181
+ return compileFailure(target, 'official', error);
182
+ }
183
+ try {
184
+ actual = normalizeResult(rsvelteCompile(source, compileOptions));
185
+ } catch (error) {
186
+ return compileFailure(target, 'rsvelte', error);
187
+ }
188
+
189
+ const differences = [];
190
+ compareArtifact(differences, 'js.code', official?.js?.code, actual?.js?.code);
191
+ compareArtifact(differences, 'css.code', official?.css?.code ?? null, actual?.css?.code ?? null);
192
+ return { target, match: differences.length === 0, differences };
193
+ }
194
+
195
+ function normalizeResult(result) {
196
+ return typeof result === 'string' ? JSON.parse(result) : result;
197
+ }
198
+
199
+ function compileFailure(target, side, error) {
200
+ return {
201
+ target,
202
+ match: false,
203
+ differences: [],
204
+ error: { side, code: error?.code ?? null, message: error?.message ?? String(error) },
205
+ };
206
+ }
207
+
208
+ function compareArtifact(differences, artifact, official, actual) {
209
+ if (official === actual) return;
210
+ const expected = official == null ? String(official) : String(official);
211
+ const received = actual == null ? String(actual) : String(actual);
212
+ const offset = firstDifference(expected, received);
213
+ const before = expected.slice(0, offset);
214
+ const line = before.split('\n').length;
215
+ const lastNewline = before.lastIndexOf('\n');
216
+ const column = offset - lastNewline;
217
+ differences.push({
218
+ artifact,
219
+ line,
220
+ column,
221
+ official: lineAt(expected, line),
222
+ rsvelte: lineAt(received, line),
223
+ });
224
+ }
225
+
226
+ function firstDifference(left, right) {
227
+ const length = Math.min(left.length, right.length);
228
+ let index = 0;
229
+ while (index < length && left[index] === right[index]) index += 1;
230
+ return index;
231
+ }
232
+
233
+ function lineAt(value, line) {
234
+ const text = value.split('\n')[line - 1] ?? '';
235
+ return text.length > 240 ? `${text.slice(0, 237)}...` : text;
236
+ }
237
+
238
+ export function buildReport(results) {
239
+ const differences = results.filter((result) => !result.match);
240
+ return {
241
+ scanned: results.length,
242
+ matched: results.length - differences.length,
243
+ different: differences.length,
244
+ files: results,
245
+ };
246
+ }
247
+
248
+ export function formatHumanReport(report) {
249
+ const noun = report.scanned === 1 ? 'file' : 'files';
250
+ const differenceNoun = report.different === 1 ? 'difference' : 'differences';
251
+ let output = `${report.files.map((file) => (file.match ? '.' : 'X')).join('')}\n\n`;
252
+ output += `${report.scanned} ${noun} scanned, ${report.matched} match, ${report.different} ${differenceNoun}\n`;
253
+ if (report.different === 0) return output;
254
+ output += '\nDifferences:\n';
255
+ for (const file of report.files.filter((item) => !item.match)) {
256
+ output += `* ${file.file}\n`;
257
+ for (const comparison of file.comparisons.filter((item) => !item.match)) {
258
+ if (comparison.error) {
259
+ output += ` [${comparison.target}] ${comparison.error.side} failed`;
260
+ if (comparison.error.code) output += ` (${comparison.error.code})`;
261
+ output += `: ${comparison.error.message}\n`;
262
+ continue;
263
+ }
264
+ for (const difference of comparison.differences) {
265
+ output += ` [${comparison.target} ${difference.artifact}] line ${difference.line}, column ${difference.column}\n`;
266
+ output += ` official: ${JSON.stringify(difference.official)}\n`;
267
+ output += ` rsvelte: ${JSON.stringify(difference.rsvelte)}\n`;
268
+ }
269
+ }
270
+ }
271
+ return output;
272
+ }
package/package.json CHANGED
@@ -2,12 +2,14 @@
2
2
  "name": "@rsvelte/compiler",
3
3
  "type": "module",
4
4
  "description": "A high-performance Rust implementation of the Svelte compiler",
5
- "version": "0.10.20",
5
+ "version": "0.11.1",
6
6
  "license": "MIT",
7
7
  "files": [
8
8
  "rsvelte_lint_bg.wasm",
9
9
  "rsvelte_lint.js",
10
- "rsvelte_lint.d.ts"
10
+ "rsvelte_lint.d.ts",
11
+ "bin/rsvelte.mjs",
12
+ "lib/compare.mjs"
11
13
  ],
12
14
  "main": "rsvelte_lint.js",
13
15
  "types": "rsvelte_lint.d.ts",
@@ -28,6 +30,17 @@
28
30
  "rust",
29
31
  "wasm"
30
32
  ],
33
+ "bin": {
34
+ "rsvelte": "bin/rsvelte.mjs"
35
+ },
36
+ "peerDependencies": {
37
+ "svelte": "^5.0.0"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "svelte": {
41
+ "optional": true
42
+ }
43
+ },
31
44
  "exports": {
32
45
  ".": {
33
46
  "types": "./rsvelte_lint.d.ts",
package/rsvelte_lint.d.ts CHANGED
@@ -55,7 +55,7 @@ export function init(): void;
55
55
 
56
56
  /**
57
57
  * Lint `source`, returning a JSON array of diagnostics:
58
- * `[{ "severity", "line", "column", "endLine", "endColumn", "code", "message" }]`.
58
+ * `[{ "severity", "line", "column", "endLine"?, "endColumn"?, "code", "message" }]`.
59
59
  * Lines are 1-indexed, columns 0-indexed (UTF-16), matching `rsvelte check`.
60
60
  */
61
61
  export function lint(source: string, filename: string): string;
package/rsvelte_lint.js CHANGED
@@ -202,7 +202,7 @@ export function init() {
202
202
 
203
203
  /**
204
204
  * Lint `source`, returning a JSON array of diagnostics:
205
- * `[{ "severity", "line", "column", "endLine", "endColumn", "code", "message" }]`.
205
+ * `[{ "severity", "line", "column", "endLine"?, "endColumn"?, "code", "message" }]`.
206
206
  * Lines are 1-indexed, columns 0-indexed (UTF-16), matching `rsvelte check`.
207
207
  * @param {string} source
208
208
  * @param {string} filename
@@ -358,10 +358,21 @@ function __wbg_get_imports() {
358
358
  const ret = arg0 === null;
359
359
  return ret;
360
360
  },
361
+ __wbg___wbindgen_is_object_a27215656b807791: function(arg0) {
362
+ const val = arg0;
363
+ const ret = typeof(val) === 'object' && val !== null;
364
+ return ret;
365
+ },
361
366
  __wbg___wbindgen_is_undefined_c05833b95a3cf397: function(arg0) {
362
367
  const ret = arg0 === undefined;
363
368
  return ret;
364
369
  },
370
+ __wbg___wbindgen_number_get_394265ed1e1b84ee: function(arg0, arg1) {
371
+ const obj = arg1;
372
+ const ret = typeof(obj) === 'number' ? obj : undefined;
373
+ getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true);
374
+ getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);
375
+ },
365
376
  __wbg___wbindgen_string_get_b0ca35b86a603356: function(arg0, arg1) {
366
377
  const obj = arg1;
367
378
  const ret = typeof(obj) === 'string' ? obj : undefined;
@@ -395,10 +406,30 @@ function __wbg_get_imports() {
395
406
  const ret = Reflect.get(arg0, arg1);
396
407
  return ret;
397
408
  }, arguments); },
409
+ __wbg_get_unchecked_6e0ad6d2a41b06f6: function(arg0, arg1) {
410
+ const ret = arg0[arg1 >>> 0];
411
+ return ret;
412
+ },
413
+ __wbg_isArray_0677c962b281d01a: function(arg0) {
414
+ const ret = Array.isArray(arg0);
415
+ return ret;
416
+ },
417
+ __wbg_keys_58421f8f96795607: function(arg0) {
418
+ const ret = Object.keys(arg0);
419
+ return ret;
420
+ },
421
+ __wbg_length_370319915dc99107: function(arg0) {
422
+ const ret = arg0.length;
423
+ return ret;
424
+ },
398
425
  __wbg_new_227d7c05414eb861: function() {
399
426
  const ret = new Error();
400
427
  return ret;
401
428
  },
429
+ __wbg_new_b667d279fd5aa943: function(arg0, arg1) {
430
+ const ret = new Error(getStringFromWasm0(arg0, arg1));
431
+ return ret;
432
+ },
402
433
  __wbg_new_da52cf8fe3429cb2: function() {
403
434
  const ret = new Object();
404
435
  return ret;
@@ -411,6 +442,9 @@ function __wbg_get_imports() {
411
442
  const ret = Reflect.set(arg0, arg1, arg2);
412
443
  return ret;
413
444
  }, arguments); },
445
+ __wbg_set_name_3bbc583faefa4193: function(arg0, arg1, arg2) {
446
+ arg0.name = getStringFromWasm0(arg1, arg2);
447
+ },
414
448
  __wbg_stack_3b0d974bbf31e44f: function(arg0, arg1) {
415
449
  const ret = arg1.stack;
416
450
  const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
@@ -418,8 +452,12 @@ function __wbg_get_imports() {
418
452
  getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
419
453
  getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
420
454
  },
455
+ __wbg_stringify_b54333f60f1e4dad: function() { return handleError(function (arg0) {
456
+ const ret = JSON.stringify(arg0);
457
+ return ret;
458
+ }, arguments); },
421
459
  __wbindgen_cast_0000000000000001: function(arg0, arg1) {
422
- // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [String], shim_idx: 4, ret: String, inner_ret: Some(String) }, mutable: false }) -> Externref`.
460
+ // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [String], shim_idx: 6, ret: String, inner_ret: Some(String) }, mutable: false }) -> Externref`.
423
461
  const ret = makeClosure(arg0, arg1, wasm_bindgen_de7100a3341595c3___convert__closures_____invoke___alloc_a941a2cfab7efbc4___string__String__alloc_a941a2cfab7efbc4___string__String__true_);
424
462
  return ret;
425
463
  },
Binary file