@symbiote-native/svelte 0.2.0 → 0.2.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.
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// svelte-adapter-dom-shim skill §16: Svelte trims only the leading/trailing whitespace of a
|
|
2
|
+
// fragment's own children - never whitespace INSIDE a single Text node, and never a
|
|
3
|
+
// whitespace-only Text node BETWEEN two siblings. Vue's compiler (and every browser) collapses
|
|
4
|
+
// both; Svelte doesn't. Confirmed against the real compiler (2026-08-17): a Text node authored
|
|
5
|
+
// as `Hello world, this is a\n long sentence.` compiles verbatim into
|
|
6
|
+
// `$.text('Hello world, this is a\n long sentence.')` - the newline and indent ship into the
|
|
7
|
+
// native text content and render as a forced line break plus stray leading spaces on device.
|
|
8
|
+
// Reproduced live on an iOS simulator before this file existed; fixed with it registered.
|
|
9
|
+
//
|
|
10
|
+
// Two hazards:
|
|
11
|
+
// 1. Wrapped sentence - a Text node with real content plus an embedded whitespace run
|
|
12
|
+
// (usually a line-wrap for readability). Fixed by collapsing every whitespace run to a
|
|
13
|
+
// single space, matching HTML/Vue.
|
|
14
|
+
// 2. Stray sibling gap - a Text node that is entirely whitespace. Compiles to a real
|
|
15
|
+
// `$.text(' ')` -> ShimText -> RCTRawText engine node: invalid outside a Text-accepting
|
|
16
|
+
// parent, and a stray paint even inside one. Fixed by deleting the node outright, since
|
|
17
|
+
// collapsing alone leaves it there as a single space.
|
|
18
|
+
//
|
|
19
|
+
// Telling an accidental gap from an intentional inline space (`<Text>{a} {b}</Text>`) needs a
|
|
20
|
+
// signal, since both are whitespace-only nodes: did the original contain a newline? A same-line
|
|
21
|
+
// space is something the author typed on purpose; a run spanning multiple source lines has, in
|
|
22
|
+
// every case this codebase has produced, come from writing each sibling on its own line. So only
|
|
23
|
+
// a whitespace-only node with a newline gets deleted; a same-line run of 2+ spaces collapses to
|
|
24
|
+
// one and stays, matching HTML's own collapse of an intentional double space.
|
|
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.
|
|
32
|
+
//
|
|
33
|
+
// Registered in svelte.config.js's `preprocess`, same as forbid-web-only-constructs.ts and
|
|
34
|
+
// scoped-styles.ts, so it covers both svelte-check/the language server and Metro's build.
|
|
35
|
+
//
|
|
36
|
+
// Line numbers are not preserved past an edited node, unlike scoped-styles.ts's blankOut trick:
|
|
37
|
+
// blankOut hides removed characters behind an equal count of `\n` because that code is gone from
|
|
38
|
+
// the output entirely; here the newlines being removed are the bug, so re-inserting them
|
|
39
|
+
// anywhere would bring it back. A diagnostic below an edited node may point a few lines off - no
|
|
40
|
+
// source map, matching the other two preprocessors, and the one place that choice costs
|
|
41
|
+
// something.
|
|
42
|
+
import { parse } from 'svelte/compiler';
|
|
43
|
+
function isRecord(value) {
|
|
44
|
+
return typeof value === 'object' && value !== null;
|
|
45
|
+
}
|
|
46
|
+
function numberAt(node, key) {
|
|
47
|
+
const value = node[key];
|
|
48
|
+
return typeof value === 'number' ? value : undefined;
|
|
49
|
+
}
|
|
50
|
+
function stringAt(node, key) {
|
|
51
|
+
const value = node[key];
|
|
52
|
+
return typeof value === 'string' ? value : undefined;
|
|
53
|
+
}
|
|
54
|
+
function fragmentNodes(ast) {
|
|
55
|
+
if (!isRecord(ast))
|
|
56
|
+
return [];
|
|
57
|
+
const fragment = ast.fragment;
|
|
58
|
+
if (!isRecord(fragment))
|
|
59
|
+
return [];
|
|
60
|
+
return Array.isArray(fragment.nodes) ? fragment.nodes : [];
|
|
61
|
+
}
|
|
62
|
+
// Same walk shape as scoped-styles.ts's collectClassEdits: a Text node can sit inside an
|
|
63
|
+
// {#if}/{#each} branch, a snippet body, or an element's children, so this descends through every
|
|
64
|
+
// child value rather than one known key.
|
|
65
|
+
function nestedNodes(node) {
|
|
66
|
+
const nested = [];
|
|
67
|
+
for (const value of Object.values(node)) {
|
|
68
|
+
if (Array.isArray(value))
|
|
69
|
+
nested.push(...value);
|
|
70
|
+
else if (isRecord(value))
|
|
71
|
+
nested.push(value);
|
|
72
|
+
}
|
|
73
|
+
return nested;
|
|
74
|
+
}
|
|
75
|
+
const WHITESPACE_RUN = /[ \t\r\n]+/g;
|
|
76
|
+
function collectEdits(nodes, edits) {
|
|
77
|
+
for (const node of nodes) {
|
|
78
|
+
if (!isRecord(node))
|
|
79
|
+
continue;
|
|
80
|
+
if (node.type === 'Text') {
|
|
81
|
+
const start = numberAt(node, 'start');
|
|
82
|
+
const end = numberAt(node, 'end');
|
|
83
|
+
const data = stringAt(node, 'data');
|
|
84
|
+
if (start !== undefined && end !== undefined && data !== undefined) {
|
|
85
|
+
const edit = textEdit(start, end, data);
|
|
86
|
+
if (edit !== undefined)
|
|
87
|
+
edits.push(edit);
|
|
88
|
+
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
collectEdits(nestedNodes(node), edits);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function textEdit(start, end, data) {
|
|
95
|
+
if (!WHITESPACE_RUN.test(data))
|
|
96
|
+
return undefined; // fast path: nothing to collapse
|
|
97
|
+
WHITESPACE_RUN.lastIndex = 0;
|
|
98
|
+
const collapsed = data.replace(WHITESPACE_RUN, ' ');
|
|
99
|
+
if (collapsed.trim() === '') {
|
|
100
|
+
// Whitespace-only node: delete it only if the original spanned a line break, else leave it
|
|
101
|
+
// as the single collapsed space.
|
|
102
|
+
return data.includes('\n') ? { start, end, text: '' } : { start, end, text: collapsed };
|
|
103
|
+
}
|
|
104
|
+
if (collapsed === data)
|
|
105
|
+
return undefined;
|
|
106
|
+
return { start, end, text: collapsed };
|
|
107
|
+
}
|
|
108
|
+
// Applied back-to-front so an earlier edit's offsets stay valid, same as scoped-styles.ts.
|
|
109
|
+
function applyEdits(content, edits) {
|
|
110
|
+
return [...edits]
|
|
111
|
+
.sort((left, right) => right.start - left.start)
|
|
112
|
+
.reduce((source, edit) => source.slice(0, edit.start) + edit.text + source.slice(edit.end), content);
|
|
113
|
+
}
|
|
114
|
+
export function collapseTextWhitespace() {
|
|
115
|
+
return {
|
|
116
|
+
markup({ content, filename }) {
|
|
117
|
+
const ast = parse(content, { filename, modern: true });
|
|
118
|
+
const edits = [];
|
|
119
|
+
collectEdits(fragmentNodes(ast), edits);
|
|
120
|
+
if (edits.length === 0)
|
|
121
|
+
return { code: content };
|
|
122
|
+
return { code: applyEdits(content, edits) };
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -68,6 +68,18 @@ function scopedStylesPreprocessor() {
|
|
|
68
68
|
);
|
|
69
69
|
return scopedStylesPromise;
|
|
70
70
|
}
|
|
71
|
+
|
|
72
|
+
// Collapses whitespace inside Text node content the same way a browser/Vue's template compiler
|
|
73
|
+
// would (svelte-adapter-dom-shim skill §16/§29) — without it, a sentence an author wrapped across
|
|
74
|
+
// source lines for readability ships a literal newline + indent into the native text content and
|
|
75
|
+
// renders as a forced line break on device. Loaded lazily for the same reason as the two above.
|
|
76
|
+
let collapseTextWhitespacePromise;
|
|
77
|
+
function collapseTextWhitespacePreprocessor() {
|
|
78
|
+
collapseTextWhitespacePromise ??= import('@symbiote-native/svelte/collapse-text-whitespace').then(
|
|
79
|
+
mod => mod.collapseTextWhitespace(),
|
|
80
|
+
);
|
|
81
|
+
return collapseTextWhitespacePromise;
|
|
82
|
+
}
|
|
71
83
|
//
|
|
72
84
|
// Svelte 5's compiler strips <script lang="ts"> types structurally, with no external type
|
|
73
85
|
// resolution needed (unlike @vue/compiler-sfc's compileScript, which needs registerTS + a real
|
|
@@ -116,9 +128,12 @@ module.exports.transform = async function transform(params) {
|
|
|
116
128
|
// Throws with a message naming the RN alternative. Deliberately BEFORE compile(), so the
|
|
117
129
|
// author sees the real diagnosis rather than a downstream symptom.
|
|
118
130
|
(await webOnlyConstructGuard()).markup({ content: params.src, filename: params.filename });
|
|
119
|
-
const
|
|
131
|
+
const stylePreprocessed = await (
|
|
120
132
|
await scopedStylesPreprocessor()
|
|
121
133
|
).markup({ content: params.src, filename: params.filename });
|
|
134
|
+
const preprocessed = await (
|
|
135
|
+
await collapseTextWhitespacePreprocessor()
|
|
136
|
+
).markup({ content: stylePreprocessed.code, filename: params.filename });
|
|
122
137
|
const code = compileSvelteFile(preprocessed.code, params.filename);
|
|
123
138
|
// Re-label as .tsx so RN's transformer processes the module exactly like app source; Metro
|
|
124
139
|
// tracks the real path separately. Matches metro-vue-transformer.cjs's identical trick.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbiote-native/svelte",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "SymbioteNative's Svelte adapter — a DOM shim over stock compiled Svelte output driving real native iOS/Android views through the same engine as every other adapter.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,6 +38,10 @@
|
|
|
38
38
|
"types": "./build/preprocessor/scoped-styles.d.ts",
|
|
39
39
|
"default": "./build/preprocessor/scoped-styles.js"
|
|
40
40
|
},
|
|
41
|
+
"./collapse-text-whitespace": {
|
|
42
|
+
"types": "./build/preprocessor/collapse-text-whitespace.d.ts",
|
|
43
|
+
"default": "./build/preprocessor/collapse-text-whitespace.js"
|
|
44
|
+
},
|
|
41
45
|
"./style-scope": {
|
|
42
46
|
"types": "./build/style-scope.d.ts",
|
|
43
47
|
"default": "./build/style-scope.js"
|
|
@@ -71,8 +75,8 @@
|
|
|
71
75
|
"@vue/runtime-core": "^3.5.13",
|
|
72
76
|
"react": "19.2.3",
|
|
73
77
|
"svelte": "^5.56.0",
|
|
74
|
-
"@symbiote-native/engine": "0.2.0",
|
|
75
78
|
"@symbiote-native/react": "0.3.0",
|
|
79
|
+
"@symbiote-native/engine": "0.2.0",
|
|
76
80
|
"@symbiote-native/test-utils": "0.1.6",
|
|
77
81
|
"@symbiote-native/vue": "0.4.0"
|
|
78
82
|
},
|