@symbiote-native/svelte 0.2.1 → 0.3.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 (66) hide show
  1. package/build/bootstrap.js +1 -1
  2. package/build/class-value.js +1 -1
  3. package/build/components/RefreshControl.svelte +4 -2
  4. package/build/components/Text.svelte +11 -2
  5. package/build/components/View.svelte +3 -1
  6. package/build/components/activity-indicator/index.svelte +8 -2
  7. package/build/components/button.svelte +8 -3
  8. package/build/components/flat-list/index.svelte +46 -11
  9. package/build/components/image/image-logic.js +13 -3
  10. package/build/components/image-background/index.svelte +8 -2
  11. package/build/components/index.d.ts +2 -2
  12. package/build/components/input-accessory-view/index.svelte +4 -1
  13. package/build/components/keyboard-avoiding-view/index.svelte +34 -11
  14. package/build/components/modal/index.svelte +4 -1
  15. package/build/components/pressable/index.svelte +10 -3
  16. package/build/components/pressable/pressable-props.d.ts +1 -1
  17. package/build/components/scroll-view/index.svelte +54 -21
  18. package/build/components/scroll-view/sticky-header.svelte +49 -16
  19. package/build/components/section-list/index.svelte +6 -1
  20. package/build/components/switch/index.svelte +14 -3
  21. package/build/components/text-input/index.svelte +22 -6
  22. package/build/components/touchable-highlight/index.svelte +114 -15
  23. package/build/components/touchable-highlight/touchable-highlight-props.d.ts +2 -0
  24. package/build/components/touchable-native-feedback/touchable-native-feedback.svelte +7 -2
  25. package/build/components/touchable-opacity/index.svelte +112 -56
  26. package/build/components/touchable-without-feedback/index.svelte +59 -4
  27. package/build/components/virtualized-list/index.svelte +233 -70
  28. package/build/components/virtualized-list/virtualized-list-props.js +8 -4
  29. package/build/components/virtualized-section-list/index.svelte +56 -9
  30. package/build/components/virtualized-section-list/index.svelte.d.ts +1 -1
  31. package/build/components/virtualized-section-list/virtualized-section-list-props.d.ts +5 -0
  32. package/build/create-portal/index.d.ts +22 -0
  33. package/build/create-portal/index.js +160 -0
  34. package/build/descriptor-to-svelte.js +15 -21
  35. package/build/dom-shim/index.d.ts +1 -0
  36. package/build/dom-shim/index.js +3 -0
  37. package/build/dom-shim/text.js +38 -1
  38. package/build/host-instance.js +7 -2
  39. package/build/index.d.ts +5 -3
  40. package/build/index.js +16 -11
  41. package/build/modules/animated/create-animated-component.d.ts +5 -0
  42. package/build/modules/animated/create-animated-component.js +131 -0
  43. package/build/modules/animated/index.d.ts +24 -14
  44. package/build/modules/animated/index.js +30 -30
  45. package/build/modules/status-bar/index.js +1 -1
  46. package/build/modules/status-bar/index.svelte +4 -1
  47. package/build/native-view-bridge.d.ts +1 -1
  48. package/build/native-view-bridge.js +1 -1
  49. package/build/preprocessor/collapse-text-whitespace.js +20 -7
  50. package/build/preprocessor/forbid-web-only-constructs.js +8 -2
  51. package/build/preprocessor/scoped-styles.js +141 -104
  52. package/build/render.js +48 -3
  53. package/build/scope-token.d.ts +3 -2
  54. package/build/scope-token.js +6 -17
  55. package/build/style-scope.d.ts +2 -1
  56. package/build/style-scope.js +10 -8
  57. package/metro-svelte-transformer.cjs +1 -1
  58. package/package.json +7 -7
  59. package/build/modules/animated/AnimatedImage.svelte +0 -115
  60. package/build/modules/animated/AnimatedImage.svelte.d.ts +0 -5
  61. package/build/modules/animated/AnimatedScrollView.svelte +0 -92
  62. package/build/modules/animated/AnimatedScrollView.svelte.d.ts +0 -17
  63. package/build/modules/animated/AnimatedText.svelte +0 -58
  64. package/build/modules/animated/AnimatedText.svelte.d.ts +0 -5
  65. package/build/modules/animated/AnimatedView.svelte +0 -95
  66. package/build/modules/animated/AnimatedView.svelte.d.ts +0 -5
@@ -0,0 +1,131 @@
1
+ // createAnimatedComponent: the Svelte twin of the React/Vue/Solid wrapper. Wraps ANY base
2
+ // component so it accepts AnimatedNodes in its props. The framework-agnostic half
3
+ // (reduceProps / readPassthroughStyle / resolveHostNode / the AnimatedProps leaf lifecycle)
4
+ // comes from @symbiote-native/engine, shared verbatim with every other adapter; only the
5
+ // lifecycle below is Svelte's.
6
+ //
7
+ // WHY IT IS PLAIN TS AND NOT A PARAMETRIZED `.svelte` FILE. The generic wrap expresses fine in
8
+ // Svelte — `<Component {...reduced} bind:this={inner} />` over a `component` prop compiles clean
9
+ // — but a `.ts` module that imports a `.svelte` file is unparseable by any tool without the
10
+ // Svelte plugin, which includes this repo's vitest and every consumer smoke test that builds an
11
+ // Animated namespace by hand (packages/navigation's drawer suite). Writing the component body
12
+ // against `svelte/internal/client` keeps `createAnimatedComponent` importable from ordinary TS
13
+ // everywhere, and leaves `modules/animated/` with no `.svelte` file at all. The internals used
14
+ // here are the exact calls the compiler itself emits for the same source — see this package's
15
+ // `svelte-internal-client.d.ts`, and §8 of the svelte-adapter-dom-shim skill for the bump
16
+ // checklist that covers them.
17
+ //
18
+ // TWO WAYS TO THE HOST NODE, because Svelte has no ref fall-through. `bind:this` on a COMPONENT
19
+ // resolves to that component's exports, never to its element:
20
+ // * a scroll container (ScrollView / FlatList / SectionList) exports an imperative handle, so
21
+ // resolveHostNode() unwraps it through getScrollNode() — the same call React and Vue make.
22
+ // Binding the leaf to the handle itself type-checks and then silently animates nothing.
23
+ // * a plain host primitive (View / Text / Image) exports nothing, so the node arrives through
24
+ // an `{@attach}` of our own: every Symbiote component forwards attachments onto its host tag
25
+ // (runes/attachments.ts), which is the adapter's documented seam to a committed node.
26
+ // A base this adapter does not own works if it does either.
27
+ //
28
+ // EVERY OTHER PROP IS FORWARDED UNTOUCHED, including snippets. Svelte snippets — `children` and
29
+ // named scoped ones such as a list's `item` / `sectionHeader` — are ordinary props, so naming any
30
+ // of them here would drop the rest: Vue's wrapper forwarded only its default slot and
31
+ // Animated.FlatList committed empty cells with no error. The only prop this wrapper consumes is
32
+ // `passthroughAnimatedPropExplicitValues`.
33
+ import { createAttachmentKey } from 'svelte/attachments';
34
+ import { derived, get, pop, push, set, spread_props, state, user_effect, } from 'svelte/internal/client';
35
+ import { createAnimatedLeafLifecycle, isNativeAnimatedAvailable, isSymbioteNode, readPassthroughStyle, reduceProps, resolveHostNode, } from '@symbiote-native/engine';
36
+ import { ShimElement } from '../../dom-shim/index.js';
37
+ // RN's prop carrying explicit (already-rasterized) values that override the animated prop in the
38
+ // COMMITTED props (sticky-header passthrough). Named once so reduce and reconcile agree.
39
+ const PASSTHROUGH_PROP = 'passthroughAnimatedPropExplicitValues';
40
+ // The three keys Svelte's own compiled `rest_props` always drops, plus the prop we consume.
41
+ const BASE_EXCLUDES = new Set([
42
+ '$$slots',
43
+ '$$events',
44
+ '$$legacy',
45
+ PASSTHROUGH_PROP,
46
+ ]);
47
+ // The leaf additionally never sees `children`: an AnimatedProps leaf rasterizes its props into
48
+ // setNativeProps on every frame, and a snippet is not a Fabric prop. It still reaches the BASE.
49
+ const LEAF_EXCLUDES = new Set([...BASE_EXCLUDES, 'children']);
50
+ // A fresh plain object each call, never the live props proxy: the leaf lifecycle stores its
51
+ // argument as the previous-props SNAPSHOT and compares the next call against it, so handing it
52
+ // one live object twice compares that object with itself and skips every reconcile forever.
53
+ function collect(props, excludes) {
54
+ const out = {};
55
+ for (const key of Object.keys(props)) {
56
+ if (excludes.has(key))
57
+ continue;
58
+ out[key] = Reflect.get(props, key);
59
+ }
60
+ return out;
61
+ }
62
+ // Reads the base's display name for the dlog label, without a cast, so a reconcile line in a log
63
+ // dump says which Animated.* it came from — the per-component labels the six hand-authored files
64
+ // used to hardcode.
65
+ function baseName(base) {
66
+ if (typeof base !== 'function' && typeof base !== 'object')
67
+ return 'Anonymous';
68
+ if (base === null)
69
+ return 'Anonymous';
70
+ const name = Reflect.get(base, 'name');
71
+ return typeof name === 'string' && name.length > 0 ? name : 'Anonymous';
72
+ }
73
+ function hostNode(instance, shim) {
74
+ const unwrapped = resolveHostNode(instance);
75
+ if (isSymbioteNode(unwrapped))
76
+ return unwrapped;
77
+ return shim?.engineNode ?? null;
78
+ }
79
+ export function createAnimatedComponent(Base) {
80
+ const label = `Animated(${baseName(Base)})`;
81
+ return function AnimatedComponent(internals, props) {
82
+ // The compiled twin of a component's own `$props()` scope: without it the `$effect`s below
83
+ // are created immediately instead of being deferred to mount, and would first run before
84
+ // the base has rendered anything.
85
+ push(props, true);
86
+ const lifecycle = createAnimatedLeafLifecycle(label);
87
+ // The `$state.raw` twin: the shim is held by IDENTITY (a deep proxy would miss the engine's
88
+ // WeakMap mirror) and read inside the reconcile effect, so the leaf rebinds when it lands.
89
+ const hostShim = state(null);
90
+ // Rasterized props for the declarative bag: every animated node replaced by its current
91
+ // value, so the first paint already carries concrete values. The per-FRAME path is separate
92
+ // (AnimatedProps.update -> setNativeProps) and never goes through this.
93
+ const reduced = derived(() => {
94
+ const out = reduceProps(collect(props, BASE_EXCLUDES));
95
+ const passthroughStyle = readPassthroughStyle(Reflect.get(props, PASSTHROUGH_PROP));
96
+ if (passthroughStyle !== undefined) {
97
+ out.style =
98
+ out.style === undefined
99
+ ? passthroughStyle
100
+ : [out.style, passthroughStyle];
101
+ }
102
+ return out;
103
+ });
104
+ // `reduceProps` rebuilds a plain string-keyed Record, so `{@attach}`'s SYMBOL keys do not
105
+ // survive it. This one is ours and rides as a second spread source; a user's own attachments
106
+ // are re-collected the same way.
107
+ const attachments = {
108
+ [createAttachmentKey()]: (node) => {
109
+ set(hostShim, node instanceof ShimElement ? node : null);
110
+ return () => set(hostShim, null);
111
+ },
112
+ };
113
+ for (const key of Object.getOwnPropertySymbols(props)) {
114
+ attachments[key] = Reflect.get(props, key);
115
+ }
116
+ const instance = Base(internals, spread_props(() => get(reduced), attachments));
117
+ // `props` and the node are read unconditionally before any branch — a guarded read would
118
+ // drop that dependency from later re-runs. reconcile() itself tolerates a null node.
119
+ user_effect(() => {
120
+ const leafProps = collect(props, LEAF_EXCLUDES);
121
+ const node = hostNode(instance, get(hostShim));
122
+ const passthrough = Reflect.get(props, PASSTHROUGH_PROP);
123
+ lifecycle.reconcile(leafProps, node, passthrough != null && isNativeAnimatedAvailable());
124
+ });
125
+ user_effect(() => () => lifecycle.teardown());
126
+ // The base's own exports ARE the wrapper's: a parent's `bind:this` on Animated.FlatList gets
127
+ // the real scrollToOffset/scrollToIndex/getScrollNode surface, and a method added to the base
128
+ // later cannot go missing here the way a hand-listed delegation would.
129
+ return pop(instance);
130
+ };
131
+ }
@@ -1,4 +1,5 @@
1
1
  import { AnimatedValue, AnimatedValueXY, AnimatedColor, timing, spring, decay, parallel, sequence, stagger, loop, delay, add, subtract, multiply, divide, modulo, diffClamp, event, forkEvent, unforkEvent } from '@symbiote-native/engine';
2
+ export { createAnimatedComponent } from './create-animated-component';
2
3
  export type { IAnimatedComponentProps } from './animated-component-props';
3
4
  export declare const Animated: {
4
5
  Value: typeof AnimatedValue;
@@ -40,19 +41,28 @@ export declare const Animated: {
40
41
  event: typeof event;
41
42
  forkEvent: typeof forkEvent;
42
43
  unforkEvent: typeof unforkEvent;
43
- View: import("svelte").Component<import("./animated-component-props").IAnimatedComponentProps, {}, "">;
44
- Text: import("svelte").Component<import("./animated-component-props").IAnimatedComponentProps, {}, "">;
45
- Image: import("svelte").Component<import("./animated-component-props").IAnimatedComponentProps, {}, "">;
44
+ View: import("svelte").Component<import("./animated-component-props").IAnimatedComponentProps, {
45
+ $on?(type: string, callback: (e: any) => void): () => void;
46
+ $set?(props: Partial<Record<string, any>>): void;
47
+ } & Record<string, any>, string>;
48
+ Text: import("svelte").Component<import("./animated-component-props").IAnimatedComponentProps, {
49
+ $on?(type: string, callback: (e: any) => void): () => void;
50
+ $set?(props: Partial<Record<string, any>>): void;
51
+ } & Record<string, any>, string>;
52
+ Image: import("svelte").Component<import("./animated-component-props").IAnimatedComponentProps, {
53
+ $on?(type: string, callback: (e: any) => void): () => void;
54
+ $set?(props: Partial<Record<string, any>>): void;
55
+ } & Record<string, any>, string>;
46
56
  ScrollView: import("svelte").Component<import("./animated-component-props").IAnimatedComponentProps, {
47
- scrollTo: (options?: {
48
- x?: number;
49
- y?: number;
50
- animated?: boolean;
51
- }) => void;
52
- scrollToEnd: (options?: {
53
- animated?: boolean;
54
- }) => void;
55
- flashScrollIndicators: () => void;
56
- getScrollNode: () => import("@symbiote-native/engine").ISymbioteNode | null;
57
- }, "">;
57
+ $on?(type: string, callback: (e: any) => void): () => void;
58
+ $set?(props: Partial<Record<string, any>>): void;
59
+ } & Record<string, any>, string>;
60
+ FlatList: import("svelte").Component<import("./animated-component-props").IAnimatedComponentProps, {
61
+ $on?(type: string, callback: (e: any) => void): () => void;
62
+ $set?(props: Partial<Record<string, any>>): void;
63
+ } & Record<string, any>, string>;
64
+ SectionList: import("svelte").Component<import("./animated-component-props").IAnimatedComponentProps, {
65
+ $on?(type: string, callback: (e: any) => void): () => void;
66
+ $set?(props: Partial<Record<string, any>>): void;
67
+ } & Record<string, any>, string>;
58
68
  };
@@ -4,32 +4,26 @@
4
4
  // interpolation and drivers come from @symbiote-native/engine (framework-agnostic, JS-driven),
5
5
  // spread in verbatim.
6
6
  //
7
- // Unlike Vue/React, there is NO generic `createAnimatedComponent(Component)` here. Vue wraps an
8
- // arbitrary base component via `h(Component, reducedProps)`; React via `createElement(Component,
9
- // childProps)` — both are ordinary runtime function calls that work on any component reference.
10
- // Svelte has no equivalent: a compiled Svelte component is not a plain function you can invoke
11
- // generically with a fresh prop bag and get back something you can further compose — wrapping
12
- // one means literally writing `<Component {...props} bind:this={ref}>` inside another `.svelte`
13
- // file. So each of the four components below (View/Text/Image/ScrollView) is its own hand-
14
- // authored `.svelte` file sharing only the non-visual reconcile logic
15
- // (animated-props-runtime.ts). `Animated.createAnimatedComponent` is a genuine, documented scope
16
- // boundary, not an oversight — see this module's own report for the full reasoning. A consumer
17
- // wanting to animate their OWN custom component should follow the same four-file pattern (rebuild
18
- // an AnimatedProps leaf from its props, reconcile on every render into the component's own host
19
- // node/handle, expose that host node via bind:this) rather than reach for a generic wrapper that
20
- // does not exist on this adapter.
7
+ // All six components come from ONE generic `createAnimatedComponent(Base)` — the same shape
8
+ // React, Vue and Solid have — applied to the ordinary components. There is no hand-authored
9
+ // `.svelte` file per animated component any more: an earlier revision of this header claimed a
10
+ // generic wrap was impossible on Svelte, and that was simply wrong. See
11
+ // create-animated-component.ts for how the wrap reaches the host node behind a base that exports
12
+ // an imperative handle (ScrollView/FlatList/SectionList) versus one that exports nothing
13
+ // (View/Text/Image), and for why it is plain TS rather than a parametrized `.svelte` file.
21
14
  //
22
- // Animated.FlatList / Animated.SectionList are also NOT implemented in this pass. Vue omits them
23
- // because it has no FlatList/SectionList base component at all; Svelte DOES have both, so this is
24
- // a narrower, more honest gap than Vue's — flagged as follow-up work, not silently dropped (see
25
- // the module's own report). Wrapping them would follow the exact same
26
- // wrap-the-real-component-via-its-exported-handle shape as AnimatedScrollView, once each exposes
27
- // (or is confirmed to need) an equivalent handle.
15
+ // A consumer animating their OWN component wraps it the same way: `createAnimatedComponent(Mine)`
16
+ // works as long as Mine either exports `getScrollNode()` or forwards `{@attach}` onto its host
17
+ // tag, which every component in this adapter does.
28
18
  import { AnimatedValue, AnimatedValueXY, AnimatedColor, AnimatedMock, Easing, Platform, timing, spring, decay, parallel, sequence, stagger, loop, delay, add, subtract, multiply, divide, modulo, diffClamp, event, forkEvent, unforkEvent, } from '@symbiote-native/engine';
29
- import AnimatedViewComponent from './AnimatedView.svelte';
30
- import AnimatedTextComponent from './AnimatedText.svelte';
31
- import AnimatedImageComponent from './AnimatedImage.svelte';
32
- import AnimatedScrollViewComponent from './AnimatedScrollView.svelte';
19
+ import View from '../../components/View.svelte';
20
+ import Text from '../../components/Text.svelte';
21
+ import Image from '../../components/image/index.svelte';
22
+ import ScrollView from '../../components/scroll-view/index.svelte';
23
+ import FlatList from '../../components/flat-list/index.svelte';
24
+ import SectionList from '../../components/section-list/index.svelte';
25
+ import { createAnimatedComponent } from './create-animated-component.js';
26
+ export { createAnimatedComponent } from './create-animated-component.js';
33
27
  // The live, JS-driven driver namespace (real frames). RN's AnimatedImplementation.
34
28
  const liveDrivers = {
35
29
  Value: AnimatedValue,
@@ -56,14 +50,20 @@ const liveDrivers = {
56
50
  };
57
51
  // RN swaps the WHOLE driver namespace for the mock when the host reports isDisableAnimations
58
52
  // (reduced motion / test env): the mock keeps the same surface but jumps each animation to its
59
- // final value synchronously, no frames. The animated COMPONENTS (View/Text/Image/ScrollView) are
60
- // live in both branches; only the drivers/value/operators/events half is swapped, exactly like
53
+ // final value synchronously, no frames. The animated COMPONENTS (View/Text/Image/ScrollView/
54
+ // FlatList/SectionList) are live in both branches; only the drivers/value/operators/events half
55
+ // is swapped, exactly like
61
56
  // RN spreading `...Animated` (impl or mock) over the same component references.
62
57
  const drivers = Platform.isDisableAnimations ? AnimatedMock : liveDrivers;
58
+ // Wrapped eagerly, unlike Vue's lazy getters: nothing in the scroll/list chain imports THIS
59
+ // module back (sticky-header builds its own Animated.View straight from the wrap), so there is
60
+ // no half-evaluated cycle to dodge.
63
61
  export const Animated = {
64
- View: AnimatedViewComponent,
65
- Text: AnimatedTextComponent,
66
- Image: AnimatedImageComponent,
67
- ScrollView: AnimatedScrollViewComponent,
62
+ View: createAnimatedComponent(View),
63
+ Text: createAnimatedComponent(Text),
64
+ Image: createAnimatedComponent(Image),
65
+ ScrollView: createAnimatedComponent(ScrollView),
66
+ FlatList: createAnimatedComponent(FlatList),
67
+ SectionList: createAnimatedComponent(SectionList),
68
68
  ...drivers,
69
69
  };
@@ -7,7 +7,7 @@
7
7
  // in a plain sibling `.ts` file rather than inside `index.svelte` itself — same pattern as
8
8
  // components/image/index.ts.
9
9
  import StatusBarComponent from './index.svelte';
10
- import { statusBarImperative, statusBarCurrentHeight } from '@symbiote-native/engine';
10
+ import { statusBarImperative, statusBarCurrentHeight, } from '@symbiote-native/engine';
11
11
  const StatusBarWithStatics = Object.assign(StatusBarComponent, statusBarImperative);
12
12
  // Android exposes the bar height as a native constant; undefined on iOS / when absent. Read
13
13
  // lazily (getter) so nothing touches native at import time; the Android engine impl resolves
@@ -10,7 +10,10 @@
10
10
  // Unlike Vue's twin (status-bar.ts), no untyped-attrs guard layer is needed: Svelte's
11
11
  // `$props()` already destructures real typed fields straight from the compiler, so there is
12
12
  // no kebab-case-attrs normalization step to run first.
13
- import { applyStatusBarProps, type IStatusBarProps } from '@symbiote-native/engine';
13
+ import {
14
+ applyStatusBarProps,
15
+ type IStatusBarProps,
16
+ } from '@symbiote-native/engine';
14
17
 
15
18
  let props: IStatusBarProps = $props();
16
19
 
@@ -1,4 +1,4 @@
1
1
  export { mount, unmount } from './render';
2
- export { mountDescriptorChildren, createDescriptorChildrenSync } from './descriptor-to-svelte';
2
+ export { mountDescriptorChildren, createDescriptorChildrenSync, } from './descriptor-to-svelte';
3
3
  export type { IDescriptorChildrenMount } from './descriptor-to-svelte';
4
4
  export type { ShimElement } from './dom-shim';
@@ -24,4 +24,4 @@
24
24
  // 'RNCSlider') that cannot be written as a literal Svelte template tag — see
25
25
  // packages/slider/src/svelte/slider/index.svelte's header for the full reasoning.
26
26
  export { mount, unmount } from './render.js';
27
- export { mountDescriptorChildren, createDescriptorChildrenSync } from './descriptor-to-svelte.js';
27
+ export { mountDescriptorChildren, createDescriptorChildrenSync, } from './descriptor-to-svelte.js';
@@ -23,12 +23,18 @@
23
23
  // a whitespace-only node with a newline gets deleted; a same-line run of 2+ spaces collapses to
24
24
  // one and stays, matching HTML's own collapse of an intentional double space.
25
25
  //
26
- // Known gap: a whitespace-only run between two siblings on ONE line with no newline
27
- // (`<View><A/> <B/></View>`) isn't caught - indistinguishable from the intentional-space case
28
- // without knowing whether the parent renders raw text. Rare in practice (§16's own audit found
29
- // only the wrapped-sentence and cross-line-gap shapes) and still caught by
30
- // `scripts/audit-svelte-stray-whitespace.mjs`, which walks the compiled output independently of
31
- // this heuristic.
26
+ // Hazard 2 is no longer THIS file's guarantee. A whitespace-only run between two siblings on
27
+ // ONE line (`<View><A/> <B/></View>`) is indistinguishable here from an intentional space -
28
+ // the missing fact is whether the parent renders raw text, which source alone cannot say.
29
+ // `dom-shim/text.ts` knows it: by the time a text node goes live its parent's engine node is
30
+ // bound, so it drops whitespace-only text under any non-text parent. That is exact, and it
31
+ // holds even for a consumer who never registers this preprocessor.
32
+ //
33
+ // So deleting the cross-line gap here is now an OPTIMIZATION - the node is never built at all -
34
+ // plus source hygiene for svelte-check. Hazard 1 (a wrapped sentence INSIDE one text node) does
35
+ // still rely on this file: it needs collapsing rather than dropping, and only source can tell an
36
+ // authored newline from one a runtime value carries. `scripts/audit-svelte-stray-whitespace.mjs`
37
+ // still walks compiled output independently of both.
32
38
  //
33
39
  // Registered in svelte.config.js's `preprocess`, same as forbid-web-only-constructs.ts and
34
40
  // scoped-styles.ts, so it covers both svelte-check/the language server and Metro's build.
@@ -72,6 +78,11 @@ function nestedNodes(node) {
72
78
  }
73
79
  return nested;
74
80
  }
81
+ // Narrower than the shim's WHITESPACE_ONLY, on purpose - do NOT "unify" them. This class drives
82
+ // COLLAPSING runs inside real text content, where `&nbsp;` / `&emsp;` are a deliberate character
83
+ // an author typed, not formatting; folding them into a plain space would change what renders.
84
+ // The shim can be wider because it only ever DROPS, and only under a parent that cannot paint
85
+ // text at all. Same reason Svelte's own regex_not_whitespace excludes nbsp.
75
86
  const WHITESPACE_RUN = /[ \t\r\n]+/g;
76
87
  function collectEdits(nodes, edits) {
77
88
  for (const node of nodes) {
@@ -99,7 +110,9 @@ function textEdit(start, end, data) {
99
110
  if (collapsed.trim() === '') {
100
111
  // Whitespace-only node: delete it only if the original spanned a line break, else leave it
101
112
  // as the single collapsed space.
102
- return data.includes('\n') ? { start, end, text: '' } : { start, end, text: collapsed };
113
+ return data.includes('\n')
114
+ ? { start, end, text: '' }
115
+ : { start, end, text: collapsed };
103
116
  }
104
117
  if (collapsed === data)
105
118
  return undefined;
@@ -33,8 +33,14 @@ const SPECIAL_ELEMENT_ADVICE = 'has no meaning under React Native and is silentl
33
33
  '(Dimensions / AppState / StatusBar / …).';
34
34
  const FORBIDDEN = new Map([
35
35
  ['SvelteHead', { label: '<svelte:head>', advice: SPECIAL_ELEMENT_ADVICE }],
36
- ['SvelteWindow', { label: '<svelte:window>', advice: SPECIAL_ELEMENT_ADVICE }],
37
- ['SvelteDocument', { label: '<svelte:document>', advice: SPECIAL_ELEMENT_ADVICE }],
36
+ [
37
+ 'SvelteWindow',
38
+ { label: '<svelte:window>', advice: SPECIAL_ELEMENT_ADVICE },
39
+ ],
40
+ [
41
+ 'SvelteDocument',
42
+ { label: '<svelte:document>', advice: SPECIAL_ELEMENT_ADVICE },
43
+ ],
38
44
  ['SvelteBody', { label: '<svelte:body>', advice: SPECIAL_ELEMENT_ADVICE }],
39
45
  [
40
46
  'HtmlTag',