@symbiote-native/svelte 0.2.0 → 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.
- package/build/bootstrap.js +1 -1
- package/build/class-value.js +1 -1
- package/build/components/RefreshControl.svelte +4 -2
- package/build/components/Text.svelte +11 -2
- package/build/components/View.svelte +3 -1
- package/build/components/activity-indicator/index.svelte +8 -2
- package/build/components/button.svelte +8 -3
- package/build/components/flat-list/index.svelte +46 -11
- package/build/components/image/image-logic.js +13 -3
- package/build/components/image-background/index.svelte +8 -2
- package/build/components/index.d.ts +2 -2
- package/build/components/input-accessory-view/index.svelte +4 -1
- package/build/components/keyboard-avoiding-view/index.svelte +34 -11
- package/build/components/modal/index.svelte +4 -1
- package/build/components/pressable/index.svelte +10 -3
- package/build/components/pressable/pressable-props.d.ts +1 -1
- package/build/components/scroll-view/index.svelte +54 -21
- package/build/components/scroll-view/sticky-header.svelte +49 -16
- package/build/components/section-list/index.svelte +6 -1
- package/build/components/switch/index.svelte +14 -3
- package/build/components/text-input/index.svelte +22 -6
- package/build/components/touchable-highlight/index.svelte +114 -15
- package/build/components/touchable-highlight/touchable-highlight-props.d.ts +2 -0
- package/build/components/touchable-native-feedback/touchable-native-feedback.svelte +7 -2
- package/build/components/touchable-opacity/index.svelte +112 -56
- package/build/components/touchable-without-feedback/index.svelte +59 -4
- package/build/components/virtualized-list/index.svelte +233 -70
- package/build/components/virtualized-list/virtualized-list-props.js +8 -4
- package/build/components/virtualized-section-list/index.svelte +56 -9
- package/build/components/virtualized-section-list/index.svelte.d.ts +1 -1
- package/build/components/virtualized-section-list/virtualized-section-list-props.d.ts +5 -0
- package/build/create-portal/index.d.ts +22 -0
- package/build/create-portal/index.js +160 -0
- package/build/descriptor-to-svelte.js +15 -21
- package/build/dom-shim/index.d.ts +1 -0
- package/build/dom-shim/index.js +3 -0
- package/build/dom-shim/text.js +38 -1
- package/build/host-instance.js +7 -2
- package/build/index.d.ts +5 -3
- package/build/index.js +16 -11
- package/build/modules/animated/create-animated-component.d.ts +5 -0
- package/build/modules/animated/create-animated-component.js +131 -0
- package/build/modules/animated/index.d.ts +24 -14
- package/build/modules/animated/index.js +30 -30
- package/build/modules/status-bar/index.js +1 -1
- package/build/modules/status-bar/index.svelte +4 -1
- package/build/native-view-bridge.d.ts +1 -1
- package/build/native-view-bridge.js +1 -1
- package/build/preprocessor/collapse-text-whitespace.d.ts +10 -0
- package/build/preprocessor/collapse-text-whitespace.js +138 -0
- package/build/preprocessor/forbid-web-only-constructs.js +8 -2
- package/build/preprocessor/scoped-styles.js +141 -104
- package/build/render.js +48 -3
- package/build/scope-token.d.ts +3 -2
- package/build/scope-token.js +6 -17
- package/build/style-scope.d.ts +2 -1
- package/build/style-scope.js +10 -8
- package/metro-svelte-transformer.cjs +17 -2
- package/package.json +11 -7
- package/build/modules/animated/AnimatedImage.svelte +0 -115
- package/build/modules/animated/AnimatedImage.svelte.d.ts +0 -5
- package/build/modules/animated/AnimatedScrollView.svelte +0 -92
- package/build/modules/animated/AnimatedScrollView.svelte.d.ts +0 -17
- package/build/modules/animated/AnimatedText.svelte +0 -58
- package/build/modules/animated/AnimatedText.svelte.d.ts +0 -5
- package/build/modules/animated/AnimatedView.svelte +0 -95
- package/build/modules/animated/AnimatedView.svelte.d.ts +0 -5
|
@@ -19,17 +19,19 @@
|
|
|
19
19
|
// node-transform to a source rewrite because Svelte's compiler exposes no AST hook.
|
|
20
20
|
//
|
|
21
21
|
// Four steps, in order:
|
|
22
|
-
// 1.
|
|
23
|
-
// <style scoped> —
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
22
|
+
// 1. cut the block out TEXTUALLY (see STYLE_TAG_PATTERN) and compile it through
|
|
23
|
+
// @symbiote-native/css-parser's `compileScopedCss`, exactly like Vue's <style scoped> —
|
|
24
|
+
// lightningcss renames every class this file owns, `card` -> `card__svelte-<hash>`, and
|
|
25
|
+
// hands back the name map; the styles register under those names in the same global
|
|
26
|
+
// registry App.css and Vue SFC blocks populate.
|
|
27
|
+
// 2. rewrite `class` in THIS file's own markup by READING that map — never by re-deriving the
|
|
28
|
+
// name, which is how the two halves used to be two implementations of "what is this class
|
|
29
|
+
// called now". Static values resolve here, at build time; a dynamic `class={expr}` is
|
|
30
|
+
// wrapped in a runtime call to `scopeSvelteClass` (../style-scope) that resolves through the
|
|
31
|
+
// SAME map, emitted beside it.
|
|
29
32
|
// 3. delete the `<style>` block from the source handed on, so Svelte emits no
|
|
30
33
|
// `css_unused_selector` warnings and adds no scope hash of its own.
|
|
31
|
-
// 4. append one `<script module>` line holding the `
|
|
32
|
-
// per-file constants step 2's rewrite refers to.
|
|
34
|
+
// 4. append one `<script module>` line holding the `registerRules()` call and the name map.
|
|
33
35
|
//
|
|
34
36
|
// SCOPING SEMANTICS, AND THE ONE DELIBERATE DIVERGENCE FROM SVELTE-ON-THE-WEB. Svelte scopes by
|
|
35
37
|
// FILE: only markup written in this file carries the scope, never markup a child component owns
|
|
@@ -52,7 +54,7 @@
|
|
|
52
54
|
// existing `<script module>` tag's own line — so every original line keeps its number and a
|
|
53
55
|
// `svelte-check` diagnostic still points at the right place.
|
|
54
56
|
import { parse } from 'svelte/compiler';
|
|
55
|
-
import {
|
|
57
|
+
import { compile as compilePreprocessor, compileScopedCss, hashFilePath, } from '@symbiote-native/css-parser';
|
|
56
58
|
// Resolved by package SELF-REFERENCE, not the relative `../scope-token` the rest of this package
|
|
57
59
|
// would write — same reason metro-svelte-transformer.cjs reaches for
|
|
58
60
|
// `@symbiote-native/svelte/preprocessor`. `svelte.config.js` is loaded directly by Node
|
|
@@ -62,19 +64,19 @@ import { classTokensIn, compile as compilePreprocessor, globalClassNamesIn, glob
|
|
|
62
64
|
// in this workspace, `build` in a published install — so one line works in both. `scope-token`
|
|
63
65
|
// specifically, never `style-scope`: that one reaches the engine through `class-value`, a whole
|
|
64
66
|
// extensionless graph this build-time file must not pull in.
|
|
65
|
-
import { scopeToken } from '@symbiote-native/svelte/scope-token';
|
|
67
|
+
import { scopeToken, } from '@symbiote-native/svelte/scope-token';
|
|
66
68
|
const SCOPE_ID_PREFIX = 'svelte-';
|
|
67
69
|
const ENGINE_MODULE = '@symbiote-native/engine';
|
|
68
70
|
const RUNTIME_MODULE = '@symbiote-native/svelte/style-scope';
|
|
69
71
|
// Every injected identifier is `__symbiote`-prefixed and import-aliased so a component that
|
|
70
|
-
// happens to declare its own `
|
|
71
|
-
const
|
|
72
|
+
// happens to declare its own `registerRules` / `scopeSvelteClass` cannot collide with it.
|
|
73
|
+
const REGISTER_RULES_LOCAL = '__symbioteRegisterRules';
|
|
72
74
|
const SCOPE_CLASS_LOCAL = '__symbioteScopeClass';
|
|
73
75
|
const SCOPED_NAMES_LOCAL = '__symbioteScopedNames';
|
|
74
|
-
const SCOPE_ID_LOCAL = '__symbioteScopeId';
|
|
75
76
|
// A `<style lang="…">` names its language directly, unlike a standalone file identified by its
|
|
76
77
|
// extension — so this is its own lookup rather than css-parser's extension-keyed
|
|
77
78
|
// `detectLanguage()`. Same table Vue's transformer keeps for the identical reason.
|
|
79
|
+
const DEFAULT_STYLE_LANG = 'css';
|
|
78
80
|
const STYLE_LANG_TO_PREPROCESSOR = new Map([
|
|
79
81
|
['css', 'css'],
|
|
80
82
|
['scss', 'scss'],
|
|
@@ -93,55 +95,82 @@ export function scopedStyles() {
|
|
|
93
95
|
// this saves them a full parse on every Metro transform.
|
|
94
96
|
if (!content.includes('<style'))
|
|
95
97
|
return { code: content };
|
|
96
|
-
const
|
|
97
|
-
const styleBlock = readStyleBlock(ast);
|
|
98
|
+
const styleBlock = findStyleBlock(content);
|
|
98
99
|
if (styleBlock === undefined)
|
|
99
100
|
return { code: content };
|
|
100
101
|
const path = filename ?? 'component.svelte';
|
|
101
102
|
const css = await compileStyleBlock(styleBlock, path);
|
|
102
|
-
const parsed = parseCSS(css, { filename: path });
|
|
103
|
-
const exemptFromScope = globalClassNamesIn(css);
|
|
104
|
-
const scopeId = SCOPE_ID_PREFIX + hashFilePath(path);
|
|
105
103
|
// A `:global(...)` selector opts out of scoping and registers under its plain name, the
|
|
106
|
-
// same escape hatch Vue's <style scoped> has.
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const isExempt = exemptFromScope.has(className);
|
|
125
|
-
const registeredName = isExempt ? className : `${className}__${scopeId}`;
|
|
126
|
-
if (!isExempt) {
|
|
127
|
-
localNames.add(className);
|
|
128
|
-
for (const token of tokensByName.get(className) ?? []) {
|
|
129
|
-
if (!globalTokens.has(token))
|
|
130
|
-
localNames.add(token);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
styles[registeredName] = props;
|
|
134
|
-
}
|
|
135
|
-
const edits = [blankOut(styleBlock.start, styleBlock.end, content)];
|
|
136
|
-
collectClassEdits(fragmentNodes(ast), content, localNames, scopeId, edits);
|
|
137
|
-
if (Object.keys(styles).length > 0) {
|
|
138
|
-
edits.push(injectModuleScript(ast, content, styles, localNames, scopeId));
|
|
104
|
+
// same escape hatch Vue's <style scoped> has. lightningcss decides that — a name it did not
|
|
105
|
+
// rename is absent from `names` and every token of it passes through untouched below.
|
|
106
|
+
const { rules, names } = compileScopedCss(css, {
|
|
107
|
+
filename: path,
|
|
108
|
+
pattern: `[local]__${SCOPE_ID_PREFIX}${hashFilePath(path)}`,
|
|
109
|
+
});
|
|
110
|
+
// Parsed WITHOUT the style block, and with it replaced by same-length whitespace, so every
|
|
111
|
+
// offset the AST reports still indexes `content` itself.
|
|
112
|
+
const ast = parse(withoutStyleBlock(content, styleBlock), {
|
|
113
|
+
filename,
|
|
114
|
+
modern: true,
|
|
115
|
+
});
|
|
116
|
+
const edits = [
|
|
117
|
+
blankOut(styleBlock.start, styleBlock.end, content),
|
|
118
|
+
];
|
|
119
|
+
collectClassEdits(fragmentNodes(ast), content, names, edits);
|
|
120
|
+
if (rules.length > 0) {
|
|
121
|
+
edits.push(injectModuleScript(ast, content, rules, names));
|
|
139
122
|
}
|
|
140
123
|
return { code: applyEdits(content, edits) };
|
|
141
124
|
},
|
|
142
125
|
};
|
|
143
126
|
}
|
|
144
127
|
// ---------------------------------------------------------------------------------------------
|
|
128
|
+
// The <style> block, located WITHOUT parsing it.
|
|
129
|
+
//
|
|
130
|
+
// This is svelte's own tag regex (src/compiler/preprocess/index.js), which is what its official
|
|
131
|
+
// `preprocess()` style hook uses to hand a block's content to a preprocessor unparsed — and the
|
|
132
|
+
// reason `svelte-preprocess` can compile SCSS at all. Reading `parse().css` instead, as this file
|
|
133
|
+
// did until 2026-08-20, validates the block as CSS whatever `lang` says: `<style lang="scss">$pad:
|
|
134
|
+
// 7px;</style>` threw `css_expected_identifier` before the language table above was ever
|
|
135
|
+
// consulted, so only SCSS that is already valid CSS (nesting) survived. A `<!-- -->` comment is an
|
|
136
|
+
// alternative of the pattern purely so a commented-out block is skipped rather than matched.
|
|
137
|
+
const STYLE_TAG_PATTERN = /<!--[^]*?-->|<style((?:\s+[^=>'"/\s]+=(?:"[^"]*"|'[^']*'|[^>\s]+)|\s+[^=>'"/\s]+)*\s*)(?:\/>|>([\S\s]*?)<\/style>)/g;
|
|
138
|
+
const LANG_ATTRIBUTE_PATTERN = /\blang\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/;
|
|
139
|
+
function findStyleBlock(content) {
|
|
140
|
+
for (const match of content.matchAll(STYLE_TAG_PATTERN)) {
|
|
141
|
+
const [text, attributes, source] = match;
|
|
142
|
+
if (attributes === undefined || source === undefined)
|
|
143
|
+
continue;
|
|
144
|
+
return {
|
|
145
|
+
start: match.index,
|
|
146
|
+
end: match.index + text.length,
|
|
147
|
+
source,
|
|
148
|
+
lang: readLang(attributes),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
function readLang(attributes) {
|
|
154
|
+
const match = LANG_ATTRIBUTE_PATTERN.exec(attributes);
|
|
155
|
+
if (match === null)
|
|
156
|
+
return DEFAULT_STYLE_LANG;
|
|
157
|
+
return match[1] ?? match[2] ?? match[3] ?? DEFAULT_STYLE_LANG;
|
|
158
|
+
}
|
|
159
|
+
// Same byte length, newlines kept: the AST's offsets stay valid against the ORIGINAL source, and
|
|
160
|
+
// svelte's own diagnostics keep their line numbers.
|
|
161
|
+
function withoutStyleBlock(content, block) {
|
|
162
|
+
const blanked = content.slice(block.start, block.end).replace(/[^\n]/g, ' ');
|
|
163
|
+
return content.slice(0, block.start) + blanked + content.slice(block.end);
|
|
164
|
+
}
|
|
165
|
+
async function compileStyleBlock(block, filename) {
|
|
166
|
+
const language = STYLE_LANG_TO_PREPROCESSOR.get(block.lang);
|
|
167
|
+
if (language === undefined) {
|
|
168
|
+
throw new Error(`${filename}: <style lang="${block.lang}"> is not supported — use css, scss, sass, less or stylus.`);
|
|
169
|
+
}
|
|
170
|
+
const path = block.lang === INDENTED_SASS_LANG ? `${filename}.sass` : filename;
|
|
171
|
+
return compilePreprocessor(block.source, language, path);
|
|
172
|
+
}
|
|
173
|
+
// ---------------------------------------------------------------------------------------------
|
|
145
174
|
// AST reading. `parse()`'s return type is not pinned here (same call, same reasoning as
|
|
146
175
|
// forbid-web-only-constructs.ts): it is read through runtime guards on `unknown` rather than
|
|
147
176
|
// trusting a shape nothing verifies.
|
|
@@ -156,43 +185,6 @@ function stringAt(node, key) {
|
|
|
156
185
|
const value = node[key];
|
|
157
186
|
return typeof value === 'string' ? value : undefined;
|
|
158
187
|
}
|
|
159
|
-
function readStyleBlock(ast) {
|
|
160
|
-
if (!isRecord(ast))
|
|
161
|
-
return undefined;
|
|
162
|
-
const css = ast.css;
|
|
163
|
-
if (!isRecord(css))
|
|
164
|
-
return undefined;
|
|
165
|
-
const start = numberAt(css, 'start');
|
|
166
|
-
const end = numberAt(css, 'end');
|
|
167
|
-
const content = css.content;
|
|
168
|
-
if (start === undefined || end === undefined || !isRecord(content))
|
|
169
|
-
return undefined;
|
|
170
|
-
const source = stringAt(content, 'styles');
|
|
171
|
-
if (source === undefined)
|
|
172
|
-
return undefined;
|
|
173
|
-
return { start, end, source, lang: readLangAttribute(css) };
|
|
174
|
-
}
|
|
175
|
-
function readLangAttribute(block) {
|
|
176
|
-
const attributes = block.attributes;
|
|
177
|
-
if (!Array.isArray(attributes))
|
|
178
|
-
return 'css';
|
|
179
|
-
for (const attribute of attributes) {
|
|
180
|
-
if (!isRecord(attribute) || attribute.name !== 'lang')
|
|
181
|
-
continue;
|
|
182
|
-
const text = staticTextOf(attribute);
|
|
183
|
-
if (text !== undefined)
|
|
184
|
-
return text;
|
|
185
|
-
}
|
|
186
|
-
return 'css';
|
|
187
|
-
}
|
|
188
|
-
async function compileStyleBlock(block, filename) {
|
|
189
|
-
const language = STYLE_LANG_TO_PREPROCESSOR.get(block.lang);
|
|
190
|
-
if (language === undefined) {
|
|
191
|
-
throw new Error(`${filename}: <style lang="${block.lang}"> is not supported — use css, scss, sass, less or stylus.`);
|
|
192
|
-
}
|
|
193
|
-
const path = block.lang === INDENTED_SASS_LANG ? `${filename}.sass` : filename;
|
|
194
|
-
return compilePreprocessor(block.source, language, path);
|
|
195
|
-
}
|
|
196
188
|
function fragmentNodes(ast) {
|
|
197
189
|
if (!isRecord(ast))
|
|
198
190
|
return [];
|
|
@@ -215,17 +207,23 @@ function nestedNodes(node) {
|
|
|
215
207
|
}
|
|
216
208
|
return nested;
|
|
217
209
|
}
|
|
218
|
-
function collectClassEdits(nodes, content,
|
|
210
|
+
function collectClassEdits(nodes, content, names, edits) {
|
|
219
211
|
for (const node of nodes) {
|
|
220
212
|
if (!isRecord(node))
|
|
221
213
|
continue;
|
|
222
214
|
if (node.type === 'Attribute' && node.name === 'class') {
|
|
223
|
-
const edit = classAttributeEdit(node, content,
|
|
215
|
+
const edit = classAttributeEdit(node, content, names);
|
|
216
|
+
if (edit !== undefined)
|
|
217
|
+
edits.push(edit);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (node.type === 'ClassDirective') {
|
|
221
|
+
const edit = classDirectiveEdit(node, content, names);
|
|
224
222
|
if (edit !== undefined)
|
|
225
223
|
edits.push(edit);
|
|
226
224
|
continue;
|
|
227
225
|
}
|
|
228
|
-
collectClassEdits(nestedNodes(node), content,
|
|
226
|
+
collectClassEdits(nestedNodes(node), content, names, edits);
|
|
229
227
|
}
|
|
230
228
|
}
|
|
231
229
|
// `value` is `true` for a bare `class` attribute, one node for a lone value, or an array mixing
|
|
@@ -254,7 +252,7 @@ function staticTextOf(attribute) {
|
|
|
254
252
|
}
|
|
255
253
|
return text;
|
|
256
254
|
}
|
|
257
|
-
function classAttributeEdit(attribute, content,
|
|
255
|
+
function classAttributeEdit(attribute, content, names) {
|
|
258
256
|
const start = numberAt(attribute, 'start');
|
|
259
257
|
const end = numberAt(attribute, 'end');
|
|
260
258
|
const parts = attributeValueParts(attribute);
|
|
@@ -265,7 +263,7 @@ function classAttributeEdit(attribute, content, localNames, scopeId) {
|
|
|
265
263
|
const scoped = staticValue
|
|
266
264
|
.split(/\s+/)
|
|
267
265
|
.filter(Boolean)
|
|
268
|
-
.map(token => scopeToken(token,
|
|
266
|
+
.map(token => scopeToken(token, names))
|
|
269
267
|
.join(' ');
|
|
270
268
|
// Nothing this file owns appears in the value — leave the source byte-identical rather than
|
|
271
269
|
// re-quoting it, so a component with only `:global(...)` rules is untouched.
|
|
@@ -275,8 +273,8 @@ function classAttributeEdit(attribute, content, localNames, scopeId) {
|
|
|
275
273
|
}
|
|
276
274
|
// A dynamic value can only be scoped at runtime, and only if this file defines something to
|
|
277
275
|
// scope against. With no local names there is nothing for the call to do, so it is not emitted
|
|
278
|
-
// at all — and the
|
|
279
|
-
if (
|
|
276
|
+
// at all — and the map it would reference stays out of the bundle.
|
|
277
|
+
if (names.size === 0)
|
|
280
278
|
return undefined;
|
|
281
279
|
const expression = expressionSourceOf(parts, content);
|
|
282
280
|
if (expression === undefined)
|
|
@@ -284,15 +282,36 @@ function classAttributeEdit(attribute, content, localNames, scopeId) {
|
|
|
284
282
|
return {
|
|
285
283
|
start,
|
|
286
284
|
end,
|
|
287
|
-
text: `class={${SCOPE_CLASS_LOCAL}(${expression}, ${SCOPED_NAMES_LOCAL}
|
|
285
|
+
text: `class={${SCOPE_CLASS_LOCAL}(${expression}, ${SCOPED_NAMES_LOCAL})}`,
|
|
288
286
|
};
|
|
289
287
|
}
|
|
288
|
+
// `class:card={cond}` carries its class name in the DIRECTIVE, where the value rewrite above
|
|
289
|
+
// cannot see it. Svelte rejects the directive on a COMPONENT (`component_invalid_directive`), so
|
|
290
|
+
// only a host element reaches here — rewritten anyway, because a token left unscoped names a rule
|
|
291
|
+
// that registered under the scoped name and is silently dead. The shorthand `class:card` expands
|
|
292
|
+
// to `class:card__<scope>={card}`: the class is scoped, the variable it reads keeps its own name.
|
|
293
|
+
function classDirectiveEdit(directive, content, names) {
|
|
294
|
+
const name = stringAt(directive, 'name');
|
|
295
|
+
const start = numberAt(directive, 'start');
|
|
296
|
+
const end = numberAt(directive, 'end');
|
|
297
|
+
if (name === undefined || start === undefined || end === undefined)
|
|
298
|
+
return undefined;
|
|
299
|
+
const scoped = scopeToken(name, names);
|
|
300
|
+
if (scoped === name)
|
|
301
|
+
return undefined;
|
|
302
|
+
const expression = sliceExpression(directive, content);
|
|
303
|
+
if (expression === undefined)
|
|
304
|
+
return undefined;
|
|
305
|
+
return { start, end, text: `class:${scoped}={${expression}}` };
|
|
306
|
+
}
|
|
290
307
|
// A lone `class={expr}` reproduces `expr` verbatim, so an arbitrary expression — a clsx array, a
|
|
291
308
|
// ternary, a function call — passes through untouched and only its RESULT gets scoped. An
|
|
292
309
|
// interpolated `class="a {b} c"` is rebuilt as the template literal Svelte itself would have
|
|
293
310
|
// concatenated, so both shapes reduce to one expression the runtime helper can take.
|
|
294
311
|
function expressionSourceOf(parts, content) {
|
|
295
|
-
if (parts.length === 1 &&
|
|
312
|
+
if (parts.length === 1 &&
|
|
313
|
+
isRecord(parts[0]) &&
|
|
314
|
+
parts[0].type === 'ExpressionTag') {
|
|
296
315
|
return sliceExpression(parts[0], content);
|
|
297
316
|
}
|
|
298
317
|
let literal = '';
|
|
@@ -326,18 +345,36 @@ function sliceExpression(part, content) {
|
|
|
326
345
|
return content.slice(start, end);
|
|
327
346
|
}
|
|
328
347
|
function escapeTemplateLiteral(text) {
|
|
329
|
-
return text
|
|
348
|
+
return text
|
|
349
|
+
.replace(/\\/g, '\\\\')
|
|
350
|
+
.replace(/`/g, '\\`')
|
|
351
|
+
.replace(/\$\{/g, '\\${');
|
|
330
352
|
}
|
|
331
353
|
// ---------------------------------------------------------------------------------------------
|
|
332
354
|
// Emitting
|
|
333
|
-
|
|
355
|
+
// `combinators` is compile-time-only — the registry matches by token subset and never reads it —
|
|
356
|
+
// so it is stripped rather than shipped in every app bundle. Same cut `@symbiote-native/css-
|
|
357
|
+
// parser`'s `serializeRules` makes for a standalone `.css` file; a Svelte `<style>` block has no
|
|
358
|
+
// reason to ship more.
|
|
359
|
+
function serializeRules(rules) {
|
|
360
|
+
return JSON.stringify(rules.map(({ tokens, specificity, order, style }) => ({
|
|
361
|
+
tokens,
|
|
362
|
+
specificity,
|
|
363
|
+
order,
|
|
364
|
+
style,
|
|
365
|
+
})));
|
|
366
|
+
}
|
|
367
|
+
function injectModuleScript(ast, content, rules, names) {
|
|
334
368
|
const lines = [
|
|
335
|
-
`import {
|
|
336
|
-
`${
|
|
369
|
+
`import { registerRules as ${REGISTER_RULES_LOCAL} } from '${ENGINE_MODULE}';`,
|
|
370
|
+
`${REGISTER_RULES_LOCAL}(${serializeRules(rules)});`,
|
|
337
371
|
];
|
|
338
|
-
if (
|
|
372
|
+
if (names.size > 0) {
|
|
339
373
|
lines.unshift(`import { scopeSvelteClass as ${SCOPE_CLASS_LOCAL} } from '${RUNTIME_MODULE}';`);
|
|
340
|
-
|
|
374
|
+
// The SAME map the static rewrite above read, shipped verbatim: a dynamic value's tokens are
|
|
375
|
+
// only known at runtime, and re-deriving their names there is the divergence this map exists
|
|
376
|
+
// to make impossible.
|
|
377
|
+
lines.push(`const ${SCOPED_NAMES_LOCAL} = new Map(${JSON.stringify([...names])});`);
|
|
341
378
|
}
|
|
342
379
|
const source = lines.join(' ');
|
|
343
380
|
// Svelte rejects a second `<script module>`, so an existing one is spliced into rather than
|
package/build/render.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// believing it is talking to the real DOM. Decided during Svelte adapter planning
|
|
4
4
|
// (2026-08-11, svelte-adapter-dom-shim skill §10): single root per process, so
|
|
5
5
|
// patchGlobals()/restoreGlobals() need no ref-counting.
|
|
6
|
-
import { mount as svelteMount, unmount as svelteUnmount } from 'svelte';
|
|
7
|
-
import { createSurface, disposeRoot, dlog, } from '@symbiote-native/engine';
|
|
6
|
+
import { mount as svelteMount, unmount as svelteUnmount, } from 'svelte';
|
|
7
|
+
import { createSurface, disposeRoot, dlog, reportUncaughtError, } from '@symbiote-native/engine';
|
|
8
8
|
import { patchGlobals, restoreGlobals } from './dom-shim/index.js';
|
|
9
9
|
import { createRootShimElement } from './root-element.js';
|
|
10
10
|
// One Svelte app per surface — same re-mount-starts-clean shape as every other adapter's
|
|
@@ -18,6 +18,22 @@ function teardown(rootTag) {
|
|
|
18
18
|
apps.delete(rootTag);
|
|
19
19
|
disposeRoot(rootTag);
|
|
20
20
|
}
|
|
21
|
+
// `transformError` is svelte's own mount-level hook for errors a `<svelte:boundary>` is about to
|
|
22
|
+
// HANDLE - upstream reaches for it to run SvelteKit's `handleError`. Svelte calls it only from
|
|
23
|
+
// Boundary.#handle_error (dom/blocks/boundary.js), i.e. only once some boundary with an `onerror`
|
|
24
|
+
// or a `failed` snippet has claimed the error, and it inherits down the boundary tree, so one
|
|
25
|
+
// hook at mount covers every boundary the app writes. We use it as a read-only tap and return the
|
|
26
|
+
// error untouched, so the `failed` snippet still receives the real thing.
|
|
27
|
+
//
|
|
28
|
+
// Why this is `dlog` and NOT reportUncaughtError - read this before "fixing" it back: writing a
|
|
29
|
+
// `<svelte:boundary>` IS the developer saying "this can throw and I am handling it here".
|
|
30
|
+
// Answering that with a full-screen redbox over the fallback the app just rendered contradicts
|
|
31
|
+
// what the app asked for. The UNCAUGHT path below still reports; the only difference is whether
|
|
32
|
+
// anyone claimed the error. Same asymmetry as the React adapter's onCaughtError.
|
|
33
|
+
function tapBoundaryError(error) {
|
|
34
|
+
dlog(() => `svelte render (caught by <svelte:boundary>): ${String(error)}`);
|
|
35
|
+
return error;
|
|
36
|
+
}
|
|
21
37
|
export function mount(rootTag, RootComponent, props) {
|
|
22
38
|
teardown(rootTag);
|
|
23
39
|
// Safe to install unconditionally - patchGlobals() is itself idempotent if a surface is
|
|
@@ -25,7 +41,36 @@ export function mount(rootTag, RootComponent, props) {
|
|
|
25
41
|
patchGlobals();
|
|
26
42
|
const surface = createSurface(rootTag);
|
|
27
43
|
const target = createRootShimElement(surface);
|
|
28
|
-
|
|
44
|
+
let svelteApp;
|
|
45
|
+
try {
|
|
46
|
+
svelteApp = svelteMount(RootComponent, {
|
|
47
|
+
target,
|
|
48
|
+
props: props ?? {},
|
|
49
|
+
transformError: tapBoundaryError,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
// Svelte has no mount-level hook for an UNCAUGHT error, so the seam is the throw itself:
|
|
54
|
+
// while a subtree is still being created, error-handling.js rethrows synchronously
|
|
55
|
+
// (`handle_error` bails out before REACTION_RAN is set, and the implicit root boundary
|
|
56
|
+
// `_mount` installs carries only a `pending` snippet, so it re-throws too). Without this the
|
|
57
|
+
// throw left the surface half-committed - nothing painted, nothing logged, a blank screen.
|
|
58
|
+
//
|
|
59
|
+
// Reported AND rethrown, deliberately: the report is the channel we control and is the whole
|
|
60
|
+
// point of this seam, while the rethrow keeps upstream's contract that `mount()` fails loudly
|
|
61
|
+
// - an app or a test harness wrapping mount() in its own try/catch must still see the error.
|
|
62
|
+
//
|
|
63
|
+
// Reaches the SYNCHRONOUS mount-time throw only. An error raised later - a reactive update,
|
|
64
|
+
// an `$effect` body, a rejected `{#await}` - is rethrown from inside svelte's own microtask
|
|
65
|
+
// flush (queue_micro_task, internal/client/dom/task.js), past any try/catch of ours; those
|
|
66
|
+
// land on the host's uncaught-exception path. `flushSync()` here would pull the mount-time
|
|
67
|
+
// `$effect` case in, at the price of running user effects BEFORE the engine's first commit,
|
|
68
|
+
// where getNativeTag() is still undefined - a worse bug than the one it closes.
|
|
69
|
+
reportUncaughtError(error, {
|
|
70
|
+
origin: 'svelte render (no <svelte:boundary>)',
|
|
71
|
+
});
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
29
74
|
apps.set(rootTag, { svelteApp });
|
|
30
75
|
return surface;
|
|
31
76
|
}
|
package/build/scope-token.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
1
|
+
/** Authored class name -> the name it registers under. One spelling: nothing camelCases now. */
|
|
2
|
+
export type IScopedNames = ReadonlyMap<string, string>;
|
|
3
|
+
export declare function scopeToken(token: string, names: IScopedNames): string;
|
package/build/scope-token.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
// The `<style>`-scoping rule for ONE class token, and nothing else
|
|
1
|
+
// The `<style>`-scoping rule for ONE class token, and nothing else: a lookup in the name map
|
|
2
|
+
// @symbiote-native/css-parser built out of lightningcss's `exports`. A token the map does not
|
|
3
|
+
// carry is one this file's `<style>` block never renamed — a `:global(...)` name, or a class
|
|
4
|
+
// forwarded from somewhere else — and is returned VERBATIM.
|
|
2
5
|
//
|
|
3
6
|
// WHY THIS IS ITS OWN FILE — it looks like it belongs in `./style-scope`, and it cannot live
|
|
4
7
|
// there. The build-time half (`preprocessor/scoped-styles.ts`) and the runtime half
|
|
@@ -14,20 +17,6 @@
|
|
|
14
17
|
//
|
|
15
18
|
// Keep this file import-free. Adding one import to it breaks `svelte-check` for every consuming
|
|
16
19
|
// app, and nothing in this package's own test run would notice.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// in (@symbiote-native/css-parser's `extractClassName` always camelCases, so `.card-title`
|
|
20
|
-
// registers as `cardTitle`) while markup idiomatically writes the kebab form. A token this file's
|
|
21
|
-
// `<style>` block does NOT define is returned VERBATIM, not camelCased — unlike Vue's rewriter,
|
|
22
|
-
// which camelCases everything it sees. The engine's `resolveOne` already falls back kebab->camel
|
|
23
|
-
// on a miss, so camelCasing here would buy nothing and would silently rename a class the author
|
|
24
|
-
// is forwarding to some other file's global rule.
|
|
25
|
-
export function scopeToken(token, localNames, scopeId) {
|
|
26
|
-
const camelToken = kebabToCamel(token);
|
|
27
|
-
return localNames.has(camelToken) ? camelToken + SCOPE_SEPARATOR + scopeId : token;
|
|
28
|
-
}
|
|
29
|
-
// Duplicated from @symbiote-native/css-parser rather than imported, for the same reason
|
|
30
|
-
// core/engine/src/style-registry/index.ts duplicates it — plus the import-free constraint above.
|
|
31
|
-
export function kebabToCamel(value) {
|
|
32
|
-
return value.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
20
|
+
export function scopeToken(token, names) {
|
|
21
|
+
return names.get(token) ?? token;
|
|
33
22
|
}
|
package/build/style-scope.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { type IScopedNames } from './scope-token';
|
|
2
|
+
export declare function scopeSvelteClass(value: unknown, names: IScopedNames): unknown;
|
package/build/style-scope.js
CHANGED
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
// scoped-styles.ts` — rewrites every STATIC class token in a component's own markup while it
|
|
3
3
|
// still has the source text. A DYNAMIC `class={expr}` has no tokens to read at build time, so
|
|
4
4
|
// its value is wrapped in a call to `scopeSvelteClass` instead. Same split Vue's scoped-style
|
|
5
|
-
// path already uses (`
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// path already uses (`renameClassTokens` in @symbiote-native/engine, symbiote-sfc-style-compiler
|
|
6
|
+
// §5).
|
|
7
|
+
// Both halves resolve every token through the SAME name map `@symbiote-native/css-parser` built
|
|
8
|
+
// from lightningcss's `exports`, applied by `./scope-token` — see that file for why the lookup is
|
|
9
|
+
// not simply declared here.
|
|
8
10
|
//
|
|
9
|
-
// WHY NOT REUSE THE ENGINE'S `
|
|
11
|
+
// WHY NOT REUSE THE ENGINE'S `renameClassTokens` DIRECTLY. Its input surface is Vue's:
|
|
10
12
|
// `string | Record<string, boolean> | Array<those>`. Svelte's is wider — `class={['card', cond &&
|
|
11
|
-
// 'on']}` leaves a literal `false` in the array, which `
|
|
12
|
-
// throws on. Widening a function the other three adapters depend on, to serve one adapter's
|
|
13
|
+
// 'on']}` leaves a literal `false` in the array, which `renameClassTokens` hands to `.split()`
|
|
14
|
+
// and throws on. Widening a function the other three adapters depend on, to serve one adapter's
|
|
13
15
|
// syntax, is the trade §22b already refused for `resolveClassName`. Instead this normalizes
|
|
14
16
|
// through the adapter's OWN clsx boundary first (`normalizeSvelteClass`) and only scopes what
|
|
15
17
|
// comes back as a class string; everything else — a resolved style object, `false`, a mixed array
|
|
@@ -17,13 +19,13 @@
|
|
|
17
19
|
import { normalizeSvelteClass } from './class-value.js';
|
|
18
20
|
import { scopeToken } from './scope-token.js';
|
|
19
21
|
const CLASS_SEPARATOR = ' ';
|
|
20
|
-
export function scopeSvelteClass(value,
|
|
22
|
+
export function scopeSvelteClass(value, names) {
|
|
21
23
|
const normalized = normalizeSvelteClass(value);
|
|
22
24
|
if (typeof normalized !== 'string')
|
|
23
25
|
return normalized;
|
|
24
26
|
return normalized
|
|
25
27
|
.split(/\s+/)
|
|
26
28
|
.filter(Boolean)
|
|
27
|
-
.map(token => scopeToken(token,
|
|
29
|
+
.map(token => scopeToken(token, names))
|
|
28
30
|
.join(CLASS_SEPARATOR);
|
|
29
31
|
}
|
|
@@ -58,7 +58,7 @@ function webOnlyConstructGuard() {
|
|
|
58
58
|
|
|
59
59
|
// The `<style>` preprocessor, loaded the same lazy way and for the same reason. Unlike the guard
|
|
60
60
|
// above (which only throws and hands the source back untouched), this one REWRITES the source —
|
|
61
|
-
// it compiles the style block into
|
|
61
|
+
// it compiles the style block into registerRules() output and scopes every `class` in the
|
|
62
62
|
// component's own markup — so its returned `code` is what compile() must be given. See
|
|
63
63
|
// src/preprocessor/scoped-styles.ts for why Svelte's own `result.css` cannot be used instead.
|
|
64
64
|
let scopedStylesPromise;
|
|
@@ -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.
|
|
3
|
+
"version": "0.3.0",
|
|
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"
|
|
@@ -59,8 +63,8 @@
|
|
|
59
63
|
},
|
|
60
64
|
"dependencies": {
|
|
61
65
|
"typescript": "~6.0.0",
|
|
62
|
-
"@symbiote-native/components": "0.
|
|
63
|
-
"@symbiote-native/css-parser": "0.
|
|
66
|
+
"@symbiote-native/components": "0.5.0",
|
|
67
|
+
"@symbiote-native/css-parser": "0.4.0"
|
|
64
68
|
},
|
|
65
69
|
"peerDependencies": {
|
|
66
70
|
"@symbiote-native/engine": ">=0.1.7",
|
|
@@ -71,10 +75,10 @@
|
|
|
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.
|
|
75
|
-
"@symbiote-native/react": "0.
|
|
76
|
-
"@symbiote-native/test-utils": "0.
|
|
77
|
-
"@symbiote-native/vue": "0.
|
|
78
|
+
"@symbiote-native/engine": "0.3.0",
|
|
79
|
+
"@symbiote-native/react": "0.4.0",
|
|
80
|
+
"@symbiote-native/test-utils": "0.2.0",
|
|
81
|
+
"@symbiote-native/vue": "0.5.0"
|
|
78
82
|
},
|
|
79
83
|
"scripts": {
|
|
80
84
|
"typecheck": "tsc --build",
|