@rsvelte/vite-plugin-svelte-native 0.1.3 → 0.2.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 (3) hide show
  1. package/envelope.js +71 -2
  2. package/index.d.ts +50 -0
  3. package/package.json +6 -6
package/envelope.js CHANGED
@@ -25,6 +25,27 @@ const WARN_HAS_START = 1 << 1;
25
25
  const WARN_HAS_END = 1 << 2;
26
26
  const WARN_HAS_FRAME = 1 << 3;
27
27
 
28
+ /**
29
+ * Validate that the byte window `[off, off + len)` lies fully inside a
30
+ * buffer of `bufLen` bytes. The encoder writes offsets/lengths as u32s
31
+ * the decoder otherwise trusts blindly — `buf.toString` / `subarray`
32
+ * silently *clamp* an out-of-range window, so a malformed envelope would
33
+ * decode to truncated data instead of failing loudly. Throw instead (M-012).
34
+ *
35
+ * @param {number} bufLen
36
+ * @param {number} off
37
+ * @param {number} len
38
+ * @param {string} label
39
+ */
40
+ function assertWindow(bufLen, off, len, label) {
41
+ if (off < 0 || len < 0 || off + len > bufLen) {
42
+ throw new Error(
43
+ `[rsvelte] envelope ${label} out of bounds ` +
44
+ `(offset ${off} + length ${len} exceeds buffer of ${bufLen} bytes)`,
45
+ );
46
+ }
47
+ }
48
+
28
49
  /**
29
50
  * Decode an envelope produced by `compileEnvelope` / `compileModuleEnvelope`.
30
51
  *
@@ -71,10 +92,24 @@ function decodeEnvelope(buf) {
71
92
  const cssMapLen = view.getUint32(44, true);
72
93
  const warningsOff = view.getUint32(48, true);
73
94
  const warningsCount = view.getUint32(52, true);
95
+ const warningsLen = view.getUint32(56, true);
74
96
 
75
97
  const hasCss = (flags & FLAG_HAS_CSS) !== 0;
76
98
  const runes = (flags & FLAG_RUNES) !== 0;
77
99
 
100
+ // Validate every field window up front so a malformed envelope throws
101
+ // at decode time rather than silently truncating on first lazy read.
102
+ // A zero offset marks an absent optional field (the header alone fills
103
+ // the first HEADER_LEN bytes, so real data never starts at offset 0).
104
+ const len = buf.byteLength;
105
+ assertWindow(len, jsCodeOff, jsCodeLen, 'js.code');
106
+ if (jsMapOff !== 0) assertWindow(len, jsMapOff, jsMapLen, 'js.map');
107
+ if (hasCss) {
108
+ assertWindow(len, cssCodeOff, cssCodeLen, 'css.code');
109
+ if (cssMapOff !== 0) assertWindow(len, cssMapOff, cssMapLen, 'css.map');
110
+ }
111
+ if (warningsCount > 0) assertWindow(len, warningsOff, warningsLen, 'warnings');
112
+
78
113
  // `buf.toString('utf8', start, end)` is the V8 fast path for
79
114
  // Buffer→string and is consistently faster than wrapping in a
80
115
  // TextDecoder; fall back to TextDecoder for plain Uint8Array.
@@ -109,7 +144,7 @@ function decodeEnvelope(buf) {
109
144
  configurable: true,
110
145
  get() {
111
146
  if (warningsCache === null) {
112
- warningsCache = decodeWarnings(buf, view, warningsOff, warningsCount, slice);
147
+ warningsCache = decodeWarnings(buf, view, warningsOff, warningsLen, warningsCount, slice);
113
148
  }
114
149
  return warningsCache;
115
150
  },
@@ -186,29 +221,50 @@ function makeCodeMapObject(buf, slice, codeOff, codeLen, mapOff, mapLen) {
186
221
  return obj;
187
222
  }
188
223
 
189
- function decodeWarnings(buf, view, off, count, slice) {
224
+ function decodeWarnings(buf, view, off, regionLen, count, slice) {
225
+ // Every record field is bounds-checked against the warnings region
226
+ // (already validated to lie inside the buffer by the caller) so a
227
+ // corrupt internal length throws rather than reading adjacent bytes
228
+ // or overrunning the buffer.
229
+ const end = off + regionLen;
230
+ const need = (p, n, what) => {
231
+ if (p < off || p + n > end) {
232
+ throw new Error(
233
+ `[rsvelte] truncated warning ${what} at offset ${p} ` +
234
+ `(needs ${n} bytes, region ends at ${end})`,
235
+ );
236
+ }
237
+ };
190
238
  const out = new Array(count);
191
239
  let p = off;
192
240
  for (let i = 0; i < count; i++) {
241
+ need(p, 4, 'code length');
193
242
  const codeLen = view.getUint32(p, true);
194
243
  p += 4;
244
+ need(p, codeLen, 'code');
195
245
  const code = slice(p, codeLen);
196
246
  p += codeLen;
247
+ need(p, 4, 'message length');
197
248
  const msgLen = view.getUint32(p, true);
198
249
  p += 4;
250
+ need(p, msgLen, 'message');
199
251
  const message = slice(p, msgLen);
200
252
  p += msgLen;
253
+ need(p, 1, 'flags');
201
254
  const flags = view.getUint8(p);
202
255
  p += 1;
203
256
 
204
257
  const w = { code, message };
205
258
  if (flags & WARN_HAS_FILENAME) {
259
+ need(p, 4, 'filename length');
206
260
  const len = view.getUint32(p, true);
207
261
  p += 4;
262
+ need(p, len, 'filename');
208
263
  w.filename = slice(p, len);
209
264
  p += len;
210
265
  }
211
266
  if (flags & WARN_HAS_START) {
267
+ need(p, 12, 'start position');
212
268
  w.start = {
213
269
  line: view.getUint32(p, true),
214
270
  column: view.getUint32(p + 4, true),
@@ -217,6 +273,7 @@ function decodeWarnings(buf, view, off, count, slice) {
217
273
  p += 12;
218
274
  }
219
275
  if (flags & WARN_HAS_END) {
276
+ need(p, 12, 'end position');
220
277
  w.end = {
221
278
  line: view.getUint32(p, true),
222
279
  column: view.getUint32(p + 4, true),
@@ -228,8 +285,10 @@ function decodeWarnings(buf, view, off, count, slice) {
228
285
  w.position = [w.start.character, w.end.character];
229
286
  }
230
287
  if (flags & WARN_HAS_FRAME) {
288
+ need(p, 4, 'frame length');
231
289
  const len = view.getUint32(p, true);
232
290
  p += 4;
291
+ need(p, len, 'frame');
233
292
  w.frame = slice(p, len);
234
293
  p += len;
235
294
  }
@@ -298,12 +357,22 @@ function decodeBatch(buf) {
298
357
  }
299
358
  const count = view.getUint32(12, true);
300
359
 
360
+ // The entry table must fit inside the buffer before we index into it.
361
+ const tableEnd = BATCH_HEADER_LEN + count * BATCH_ENTRY_LEN;
362
+ if (tableEnd > buf.byteLength) {
363
+ throw new Error(
364
+ `[rsvelte] batch entry table (${count} entries) exceeds buffer ` +
365
+ `(needs ${tableEnd} bytes, buffer is ${buf.byteLength})`,
366
+ );
367
+ }
368
+
301
369
  const out = new Array(count);
302
370
  for (let i = 0; i < count; i++) {
303
371
  const entryOff = BATCH_HEADER_LEN + i * BATCH_ENTRY_LEN;
304
372
  const status = view.getUint32(entryOff, true);
305
373
  const payloadOff = view.getUint32(entryOff + 4, true);
306
374
  const payloadLen = view.getUint32(entryOff + 8, true);
375
+ assertWindow(buf.byteLength, payloadOff, payloadLen, `batch entry ${i}`);
307
376
  // Subarray gives a zero-copy view into the same underlying
308
377
  // ArrayBuffer; `decodeEnvelope` walks it without further allocation.
309
378
  const slice = buf.subarray
package/index.d.ts CHANGED
@@ -281,6 +281,56 @@ export function compileModuleLegacy(
281
281
  options?: ModuleCompileOptions,
282
282
  ): CompileResult;
283
283
 
284
+ // ---------------------------------------------------------------------------
285
+ // parse
286
+ // ---------------------------------------------------------------------------
287
+
288
+ export interface ParseOptions {
289
+ /**
290
+ * Skip emitting nested `loc: { start, end }` blocks on every embedded
291
+ * JavaScript expression. Top-level `start` / `end` byte offsets are
292
+ * still emitted. Callers that re-parse expression ranges with their
293
+ * own parser (e.g. `svelte-eslint-parser`) get a smaller AST and a
294
+ * meaningfully faster `JSON.parse` (with the JSON path) or a tighter
295
+ * binary buffer (with `parseEnvelope`).
296
+ */
297
+ skipExpressionLoc?: boolean;
298
+ /**
299
+ * Skip emitting the full CSS `StyleSheet` AST. The decoded `css`
300
+ * field becomes a minimal stub
301
+ * (`{ type: "StyleSheet", start, end, attributes: [], children: [], content: { start, end, styles: "", comment: null } }`).
302
+ * Use when the downstream pipeline re-parses `<style>` blocks with
303
+ * its own CSS parser (e.g. `svelte-eslint-parser` uses postcss).
304
+ * Saves ~5–10 KB of buffer per component and skips the matching
305
+ * JSON-parse step.
306
+ */
307
+ skipCssAst?: boolean;
308
+ }
309
+
310
+ /**
311
+ * Parse a Svelte component and return the AST as a JSON string. The
312
+ * caller is responsible for `JSON.parse` on the returned value.
313
+ *
314
+ * For the fastest path skip JSON entirely: use {@link parseEnvelope}
315
+ * with `decodeParseEnvelope` from
316
+ * `@rsvelte/vite-plugin-svelte-native/parse-envelope.js`.
317
+ */
318
+ export function parse(source: string, options?: ParseOptions): string;
319
+
320
+ /**
321
+ * Parse a Svelte component and return a raw-transfer envelope `Buffer`.
322
+ *
323
+ * The buffer is a compact binary encoding of the AST (all 26 template
324
+ * node types and all 74 estree node types are tagged; only a small
325
+ * number of leaf sub-trees — CSS, `SvelteOptions`, directive metadata —
326
+ * fall back to inline JSON behind `TAG_JSON`). Pair with
327
+ * `decodeParseEnvelope` exported from
328
+ * `@rsvelte/vite-plugin-svelte-native/parse-envelope.js` to get the
329
+ * same plain-object AST that `JSON.parse(parse(source))` produces, but
330
+ * without the per-byte `JSON.parse` tokenisation cost.
331
+ */
332
+ export function parseEnvelope(source: string, options?: ParseOptions): Buffer;
333
+
284
334
  // ---------------------------------------------------------------------------
285
335
  // svelte2tsx
286
336
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rsvelte/vite-plugin-svelte-native",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "NAPI bindings to the rsvelte compiler — used by the @rsvelte vite-plugin-svelte shim",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -27,11 +27,11 @@
27
27
  "index.d.ts"
28
28
  ],
29
29
  "optionalDependencies": {
30
- "@rsvelte/vite-plugin-svelte-native-darwin-arm64": "^0.1.3",
31
- "@rsvelte/vite-plugin-svelte-native-darwin-x64": "^0.1.3",
32
- "@rsvelte/vite-plugin-svelte-native-linux-arm64-gnu": "^0.1.3",
33
- "@rsvelte/vite-plugin-svelte-native-linux-x64-gnu": "^0.1.3",
34
- "@rsvelte/vite-plugin-svelte-native-win32-x64-msvc": "^0.1.3"
30
+ "@rsvelte/vite-plugin-svelte-native-darwin-arm64": "^0.2.0",
31
+ "@rsvelte/vite-plugin-svelte-native-darwin-x64": "^0.2.0",
32
+ "@rsvelte/vite-plugin-svelte-native-linux-arm64-gnu": "^0.2.0",
33
+ "@rsvelte/vite-plugin-svelte-native-linux-x64-gnu": "^0.2.0",
34
+ "@rsvelte/vite-plugin-svelte-native-win32-x64-msvc": "^0.2.0"
35
35
  },
36
36
  "publishConfig": {
37
37
  "access": "public"