svelte-shaker 0.9.0 → 0.9.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.
package/dist/analyze.js CHANGED
@@ -567,6 +567,9 @@ function collectEscapedComponents(ast, imports, importedLocals, namespaceLocals)
567
567
  };
568
568
  walk(ast.fragment, { parent: null }, {
569
569
  _(node, { state, next }) {
570
+ // Type-only subtrees are erased at compile — never a runtime escape.
571
+ if (isTypeOnlyNode(node))
572
+ return;
570
573
  if (node.type === 'Identifier' &&
571
574
  node.name &&
572
575
  importedLocals.has(node.name) &&
@@ -581,6 +584,11 @@ function collectEscapedComponents(ast, imports, importedLocals, namespaceLocals)
581
584
  if (ast.instance) {
582
585
  walk(ast.instance, { parent: null }, {
583
586
  _(node, { state, next }) {
587
+ // Skip TS type positions: an identifier in `ComponentProps<typeof X>`
588
+ // or `: Props` is type-level (erased at compile), not a value read, so
589
+ // descending would falsely flag the component as escaped.
590
+ if (isTypeOnlyNode(node))
591
+ return;
584
592
  if (node.type === 'Identifier' &&
585
593
  node.name &&
586
594
  (imports.has(node.name) || namespaceLocals.has(node.name)) &&
@@ -594,6 +602,23 @@ function collectEscapedComponents(ast, imports, importedLocals, namespaceLocals)
594
602
  }
595
603
  return escaped;
596
604
  }
605
+ /**
606
+ * A TS type-only subtree the escape walk must NOT descend into: every `TSType*`
607
+ * node (type annotations, type references/queries, type-argument and
608
+ * type-parameter lists, …) plus `interface` declarations. Identifiers inside
609
+ * them — e.g. `Button` in `ComponentProps<typeof Button>['pattern']`, or `Props`
610
+ * in `: Props` — are type-level, erased at compile, never runtime value reads, so
611
+ * descending would falsely flag the component as escaped and bail it whole.
612
+ *
613
+ * `TSAsExpression` / `TSSatisfiesExpression` / `TSNonNullExpression` /
614
+ * `TSInstantiationExpression` are deliberately NOT pruned: they wrap a real
615
+ * runtime expression (`Button as T` IS a value use of `Button`), and their own
616
+ * type child is itself a `TSType*` node this prunes.
617
+ */
618
+ function isTypeOnlyNode(node) {
619
+ return (typeof node.type === 'string' &&
620
+ (node.type.startsWith('TSType') || node.type === 'TSInterfaceDeclaration'));
621
+ }
597
622
  /**
598
623
  * Is this Identifier used as a runtime *value* (so a component name here would
599
624
  * escape)? Property keys, member names and import/export specifier slots are
Binary file
package/dist/transform.js CHANGED
@@ -274,12 +274,17 @@ outDead) {
274
274
  * or gain a space.
275
275
  */
276
276
  function foldIfBlocks(fragment, env, setEnv, code, s, dead) {
277
- walk(fragment, { parent: null, preserve: hasPreserveWhitespaceOption(fragment) }, {
277
+ walk(fragment, { parent: null, preserve: hasPreserveWhitespaceOption(fragment), element: null }, {
278
278
  _(node, { state, next }) {
279
279
  if (node.type !== 'IfBlock') {
280
- // Descend, recording this node as the children's parent and whether it
281
- // opens a preserved-whitespace context for them.
282
- next({ parent: node, preserve: state.preserve || isPreserveElement(node) });
280
+ // Descend, recording this node as the children's parent, whether it
281
+ // opens a preserved-whitespace context, and the content-model element
282
+ // their seam would land in (for the `{" "}` validity check).
283
+ next({
284
+ parent: node,
285
+ preserve: state.preserve || isPreserveElement(node),
286
+ element: childParentElement(node, state.element),
287
+ });
283
288
  return;
284
289
  }
285
290
  // `elseif` IfBlocks are the *continuation* of a chain we already own from
@@ -294,9 +299,12 @@ function foldIfBlocks(fragment, env, setEnv, code, s, dead) {
294
299
  // node as its children's parent), so its `nodes` are the chain's siblings.
295
300
  index: state.parent?.nodes?.indexOf(node) ?? -1,
296
301
  preserve: state.preserve,
302
+ element: state.element,
297
303
  });
304
+ // kept head: descend for nested blocks. The `{#if}` is transparent to the
305
+ // content model, so children stay in the same parent element.
298
306
  if (decision.recurse)
299
- next({ parent: node, preserve: state.preserve }); // kept head: descend for nested blocks
307
+ next({ parent: node, preserve: state.preserve, element: state.element });
300
308
  // otherwise the subtree is gone or re-emitted verbatim — do not recurse.
301
309
  },
302
310
  });
@@ -366,10 +374,12 @@ function isFullRemoval(decision) {
366
374
  * edge-trimmed, so it renders exactly one space wherever it lands, matching the
367
375
  * original. Otherwise plain deletion already preserves space presence (only the
368
376
  * run LENGTH can differ, which the SSR oracle normalizes). Never compensate
369
- * under preserved whitespace — there plain deletion is byte-exact.
377
+ * under preserved whitespace — there plain deletion is byte-exact — nor inside a
378
+ * text-free parent (`<tr>`, `<tbody>`, …), where Svelte rejects the `{" "}` text
379
+ * child outright and the whitespace rendered nothing to begin with.
370
380
  */
371
381
  function removeChain(removed, span, code, s, dead, ctx) {
372
- if (!ctx.preserve && ctx.parent?.nodes && ctx.index >= 0) {
382
+ if (!ctx.preserve && !isTextFreeParent(ctx.element) && ctx.parent?.nodes && ctx.index >= 0) {
373
383
  const seam = analyzeSeam(ctx.parent.nodes, ctx.index, span, code, dead);
374
384
  if (seam) {
375
385
  s.overwrite(seam[0], seam[1], '{" "}');
@@ -431,6 +441,49 @@ function isRenderingSibling(node, code) {
431
441
  function isPreserveElement(node) {
432
442
  return node.type === 'RegularElement' && (node.name === 'pre' || node.name === 'textarea');
433
443
  }
444
+ /**
445
+ * Parent elements whose HTML content model forbids a text child: Svelte's
446
+ * `is_tag_valid_with_parent('#text', …)` rejects a `#text`/`{" "}` here with
447
+ * `node_invalid_placement` — these are exactly its `disallowed_children` entries
448
+ * that carry an `only` list (html/head/frameset/#document can't appear as
449
+ * elements inside a component, so only the table parts remain). Svelte never
450
+ * renders inter-child whitespace inside them either, so a removed chain's seam
451
+ * needs plain deletion: emitting the `{" "}` compensation would produce a
452
+ * component that fails to compile. See {@link removeChain}.
453
+ */
454
+ const TEXT_FREE_PARENTS = new Set(['table', 'thead', 'tbody', 'tfoot', 'tr', 'colgroup']);
455
+ /**
456
+ * Node types that reset the content-model parent to "unknown" (text allowed
457
+ * again), mirroring svelte's `parent_element: null` reset in the SvelteElement /
458
+ * SvelteFragment / SnippetBlock / Component visitors. A `{" "}` seam in any of
459
+ * these contexts is valid, so the seam compensation may proceed.
460
+ */
461
+ const PARENT_ELEMENT_RESET = new Set([
462
+ 'SvelteElement',
463
+ 'SvelteFragment',
464
+ 'SnippetBlock',
465
+ 'Component',
466
+ 'SvelteSelf',
467
+ 'SvelteComponent',
468
+ ]);
469
+ /**
470
+ * The content-model parent element a seam would land in for `node`'s children,
471
+ * given the element the walk is currently inside. Mirrors svelte's
472
+ * `parent_element` threading: a `RegularElement` becomes the parent, the reset
473
+ * node types clear it, and every other node (Fragment, blocks, …) is transparent
474
+ * and inherits. `null` means "text allowed" (root or a reset context).
475
+ */
476
+ function childParentElement(node, current) {
477
+ if (node.type === 'RegularElement')
478
+ return node.name ?? null;
479
+ if (PARENT_ELEMENT_RESET.has(node.type))
480
+ return null;
481
+ return current;
482
+ }
483
+ /** True when an `{" "}` seam would be an invalid text child of `element`. */
484
+ function isTextFreeParent(element) {
485
+ return element !== null && TEXT_FREE_PARENTS.has(element);
486
+ }
434
487
  /** Does the component opt into preserved whitespace via `<svelte:options>`? */
435
488
  function hasPreserveWhitespaceOption(fragment) {
436
489
  let preserve = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-shaker",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Tree shaking for Svelte components",
5
5
  "keywords": [
6
6
  "dead-code-elimination",