@symbiote-native/vue 3.3.0 → 3.5.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 (49) hide show
  1. package/build/components/flat-list/index.d.ts +4 -3
  2. package/build/components/flat-list/index.js +18 -21
  3. package/build/components/input-accessory-view-props.d.ts +4 -4
  4. package/build/components/keyboard-avoiding-view.d.ts +11 -3
  5. package/build/components/keyboard-avoiding-view.js +38 -69
  6. package/build/components/layout-conformance-props.d.ts +4 -0
  7. package/build/components/layout-conformance-props.js +1 -0
  8. package/build/components/modal/index.d.ts +18 -5
  9. package/build/components/modal/index.js +53 -24
  10. package/build/components/refresh-control-props.d.ts +7 -7
  11. package/build/components/scroll-view/scroll-view-props.d.ts +8 -5
  12. package/build/components/section-list/index.d.ts +1 -1
  13. package/build/components/section-list/index.js +2 -6
  14. package/build/components/text-props.d.ts +3 -3
  15. package/build/components/touchable-props.d.ts +6 -6
  16. package/build/components/virtualized-list/index.d.ts +5 -75
  17. package/build/components/virtualized-list/index.js +130 -558
  18. package/build/components/virtualized-list/list-children.d.ts +18 -0
  19. package/build/components/virtualized-list/list-children.js +81 -0
  20. package/build/components/virtualized-list/list-elements.d.ts +7 -0
  21. package/build/components/virtualized-list/list-elements.js +29 -0
  22. package/build/components/virtualized-list/list-scroll-props.d.ts +20 -0
  23. package/build/components/virtualized-list/list-scroll-props.js +9 -0
  24. package/build/components/virtualized-list/narrow-props.d.ts +79 -0
  25. package/build/components/virtualized-list/narrow-props.js +89 -0
  26. package/build/components/virtualized-list/nested-scope.d.ts +3 -0
  27. package/build/components/virtualized-list/nested-scope.js +12 -0
  28. package/build/components/virtualized-list/user-handlers.d.ts +3 -0
  29. package/build/components/virtualized-list/user-handlers.js +24 -0
  30. package/build/components/virtualized-list/virtualized-list-props.d.ts +82 -0
  31. package/build/components/virtualized-list/virtualized-list-props.js +41 -0
  32. package/build/components/virtualized-section-list/index.d.ts +20 -5
  33. package/build/components/virtualized-section-list/index.js +81 -95
  34. package/build/components/virtualized-section-list/section-item-cell.d.ts +18 -0
  35. package/build/components/virtualized-section-list/section-item-cell.js +52 -0
  36. package/build/composables/use-animated-value.d.ts +7 -0
  37. package/build/composables/use-animated-value.js +14 -0
  38. package/build/composables/use-color-scheme.d.ts +1 -1
  39. package/build/create-transition/index.d.ts +2 -2
  40. package/build/create-transition/index.js +1 -1
  41. package/build/index.d.ts +13 -4
  42. package/build/index.js +16 -31
  43. package/build/modules/animated/index.d.ts +54 -41
  44. package/build/modules/animated/index.js +2 -26
  45. package/build/modules/app-registry/index.d.ts +3 -3
  46. package/build/modules/app-registry/index.js +2 -2
  47. package/build/modules/status-bar.js +22 -24
  48. package/metro-vue-transformer.cjs +177 -229
  49. package/package.json +6 -6
@@ -1,14 +1,11 @@
1
- // Metro babel transformer that teaches the bundler to read Vue SFCs (.vue). Metro has no Vue
2
- // plugin (unplugin-vue only ships vite/webpack/esbuild/rollup adapters), so we do the
3
- // single-pass compile here: parse the SFC, compile <script setup> + <template> into one
4
- // component module, then hand the JS to RN's own babel transformer - the Metro twin of
5
- // @vitejs/plugin-vue. Imports get retargeted from 'vue' to @symbiote-native/vue/runtime-helpers
6
- // (not bare @vue/runtime-core) because that shim also supplies our own `vShow` - compiled
7
- // v-show imports it by name, and only @vue/runtime-dom's DOM-based version exists otherwise.
8
- //
9
- // Ships as a package-level export (`@symbiote-native/vue/metro-vue-transformer`) rather than
10
- // living in each consuming app: a consumer's own metro.config.js just points
11
- // babelTransformerPath at it.
1
+ // Metro babel transformer that reads Vue SFCs (.vue), the Metro twin of @vitejs/plugin-vue
2
+ // Metro has no Vue plugin, so the single-pass compile happens here before RN's own transformer
3
+
4
+ // Imports of 'vue' go to `@symbiote-native/vue/runtime-helpers`, not `@vue/runtime-core`
5
+ // That shim supplies our own `vShow`, which compiled v-show imports by name
6
+
7
+ // Ships as the package export `@symbiote-native/vue/metro-vue-transformer`
8
+ // A consumer's metro.config.js points `babelTransformerPath` at it
12
9
 
13
10
  const nodeFs = require('fs');
14
11
  const {
@@ -23,32 +20,26 @@ const {
23
20
  createSimpleExpression,
24
21
  } = require('@vue/compiler-core');
25
22
 
26
- // A bare-specifier type import (`import type { X } from '@symbiote-native/navigation/vue'`) needs
27
- // real node_modules resolution to turn the specifier into a file path - compileScript's own `fs`
28
- // option only reads paths it's already given, it can't resolve. registerTS (the same hook
29
- // @vitejs/plugin-vue and vue-tsc use) hands the compiler a lazy `typescript` loader that resolves
30
- // via `ts.resolveModuleName` and self-supplies `ts.sys` as the fs fallback.
23
+ // A bare-specifier type import needs node_modules resolution that the `fs` option cannot do
24
+ // `registerTS` gives the compiler a lazy `typescript` loader that resolves it, like plugin-vue
31
25
  registerTS(() => require('typescript'));
32
- // Required directly (not via the ./metro-css-parser public subpath, which exists for CONSUMERS)
33
- // so it resolves from this package's own node_modules under pnpm.
26
+ // Required directly, not via the ./metro-css-parser subpath that exists for CONSUMERS
27
+ // so it resolves from this package's own node_modules under pnpm
34
28
  const {
35
29
  compile: compilePreprocessor,
36
- compileCssFile,
37
30
  compileCssModule,
38
31
  compileCssToRules,
39
32
  compileScopedCss,
33
+ createCssMetroTransformer,
40
34
  hashFilePath,
41
- isStyleFile,
35
+ relabelForUpstream,
42
36
  resolveUpstreamTransformer,
43
37
  } = require('@symbiote-native/css-parser');
44
38
 
45
39
  const upstreamTransformer = resolveUpstreamTransformer();
46
40
 
47
- // compileScript needs file system access to resolve a type-only `defineProps<ISomeProps>()`
48
- // imported from another file - without it, Metro's worker process (a real Node process, but one
49
- // @vue/compiler-sfc doesn't auto-detect without registerTS) throws "No fs option provided ...
50
- // in non-Node environment". Wiring Node's own `fs` straight through is simpler than a second
51
- // registerTS-style loader.
41
+ // `compileScript` reads other files to resolve a type-only `defineProps<ISomeProps>()`
42
+ // Metro's worker is not detected as Node, so without this it throws "No fs option provided"
52
43
  const compileScriptFs = {
53
44
  fileExists: file => nodeFs.existsSync(file),
54
45
  readFile: file => {
@@ -60,29 +51,23 @@ const compileScriptFs = {
60
51
  },
61
52
  };
62
53
 
63
- // Rewrites a Vue template AST so every `class`/`:class` binding names a class by the name
64
- // lightningcss RENAMED it to. AST-level, not a raw-text regex: Vue merges a static class= and a
65
- // dynamic :class= on the same element into ONE codegen entry, and text substitution can't
66
- // reproduce that merge safely - letting Vue's own transformElement do the merge on our rewritten
67
- // nodes reuses that logic.
68
- //
69
- // `renames` is compileScopedCss's own name map (authored name -> renamed name), NOT a set of
70
- // names this file re-suffixes. That is the point of the migration: the rewriter and the style
71
- // compiler used to derive the scoped name independently, so any disagreement between them was
72
- // silent. Now there is one source and this half only reads it. One entry per class, keyed as
73
- // AUTHORED — `class="section-label"` is the lookup, `sectionLabel` is not a second spelling of it.
74
- //
75
- // A static class="foo bar" (AttributeNode, prop.type === 6) resolves to its final string here
76
- // at compile time - no runtime call needed. A token the map does not carry belongs to another
77
- // file (App.css, a `:global()` escape hatch, a parent's class) and passes through untouched.
78
- //
79
- // A dynamic :class="expr" (bind DirectiveNode, prop.type === 7) targeting `class`: by the time
80
- // our transform runs, Vue's own transformExpression has already turned prop.exp into either a
81
- // COMPOUND_EXPRESSION (type 8) or, for a bare identifier binding, a SIMPLE_EXPRESSION (type 4).
82
- // createCompoundExpression wraps the original exp node as-is inside
83
- // renameClassTokens(<original>, __scopedClassNames), so codegen reproduces the original
84
- // expression unchanged - no per-shape branching needed, and a fully opaque runtime value
85
- // (`:class="dynamicClass"`) still resolves against the same map at runtime.
54
+ // Shared with ./babel-jsx.cjs: Vue's two compilers must give one answer to element-vs-component
55
+ const INTRINSIC_TAGS = require('./intrinsic-tags.cjs');
56
+
57
+ const isIntrinsicTag = tag => INTRINSIC_TAGS.has(tag);
58
+
59
+ const CLASS_PROP = 'class';
60
+ const IMPORT_DECLARATION = 'ImportDeclaration';
61
+ const PLAIN_CSS_LANG = 'css';
62
+
63
+ // Rewrites every `class` and `:class` binding to the name lightningcss renamed it to
64
+ // It works on the AST because Vue merges a static and a dynamic class into one codegen entry
65
+
66
+ // `renames` is the `compileScopedCss` map of authored name to renamed name
67
+ // A token it lacks belongs to another file (App.css, `:global()`, a parent) and passes through
68
+
69
+ // A dynamic `:class` is wrapped as `__scopeClass(<original>, __scopedClassNames)` by Vue's codegen
70
+ // So any expression shape, even an opaque runtime value, resolves against the same map
86
71
  function createScopeClassNodeTransform(renames) {
87
72
  return function scopeClassNodeTransform(node) {
88
73
  if (node.type !== 1 /* NodeTypes.ELEMENT */) return;
@@ -90,7 +75,7 @@ function createScopeClassNodeTransform(renames) {
90
75
  for (const prop of node.props) {
91
76
  if (
92
77
  prop.type === 6 /* NodeTypes.ATTRIBUTE */ &&
93
- prop.name === 'class' &&
78
+ prop.name === CLASS_PROP &&
94
79
  prop.value
95
80
  ) {
96
81
  prop.value.content = prop.value.content
@@ -101,14 +86,7 @@ function createScopeClassNodeTransform(renames) {
101
86
  continue;
102
87
  }
103
88
 
104
- if (
105
- prop.type === 7 /* NodeTypes.DIRECTIVE */ &&
106
- prop.name === 'bind' &&
107
- prop.arg &&
108
- prop.arg.type === 4 /* NodeTypes.SIMPLE_EXPRESSION */ &&
109
- prop.arg.content === 'class' &&
110
- prop.exp
111
- ) {
89
+ if (isClassBinding(prop)) {
112
90
  prop.exp = createCompoundExpression(
113
91
  ['__scopeClass(', prop.exp, ', __scopedClassNames)'],
114
92
  prop.exp.loc,
@@ -118,14 +96,23 @@ function createScopeClassNodeTransform(renames) {
118
96
  };
119
97
  }
120
98
 
121
- // The parser's own condense() collapses a text node's multi-line whitespace to a single boundary
122
- // space but never trims it: `<text>\n plain\n</text>` compiles to " plain " instead of "plain" -
123
- // a real leading/trailing space on the rendered RN Text. Babel's JSX whitespace cleaner
124
- // (cleanJSXElementLiteralChild, used by every other adapter) DOES trim this; Vue's does not, so we
125
- // do it here, once, for every template. Matches Babel's own rule: only a text node whose SOURCE
126
- // crosses a line break is touched, so a genuinely single-line `<text>Score: {{ x }}</text>` keeps
127
- // its intentional trailing space untouched (see examples/vue-sfc's CompoundClassDemo.vue for the
128
- // device-measured bug this closes).
99
+ // By now Vue's `transformExpression` made `prop.exp` a compound or a simple expression
100
+ function isClassBinding(prop) {
101
+ return (
102
+ prop.type === 7 /* NodeTypes.DIRECTIVE */ &&
103
+ prop.name === 'bind' &&
104
+ prop.arg &&
105
+ prop.arg.type === 4 /* NodeTypes.SIMPLE_EXPRESSION */ &&
106
+ prop.arg.content === CLASS_PROP &&
107
+ Boolean(prop.exp)
108
+ );
109
+ }
110
+
111
+ // The parser's `condense()` collapses multi-line whitespace to one space but never trims it
112
+ // `<text>\n plain\n</text>` compiles to " plain ", a real space on the rendered RN Text
113
+
114
+ // Babel's JSX whitespace cleaner trims it for every other adapter, so we do it once per template
115
+ // Only a text node whose source crosses a line break is touched, as Babel does
129
116
  function trimReflowedTextNodeTransform(node) {
130
117
  if (node.type !== 1 /* NodeTypes.ELEMENT */ || node.children.length === 0)
131
118
  return;
@@ -147,12 +134,10 @@ function trimReflowedTextNodeTransform(node) {
147
134
  }
148
135
  }
149
136
 
150
- // Shared with ./babel-jsx.cjs: Vue's two compilers must give one answer to element-vs-component.
151
- const INTRINSIC_TAGS = require('./intrinsic-tags.cjs');
152
-
153
- const SYMBIOTE_SOURCE_PREFIX = '@symbiote-native/';
137
+ // The scope of this package's own name, `@symbiote-native/`, so the prefix has one source
138
+ const SYMBIOTE_SOURCE_PREFIX = `${require('./package.json').name.split('/')[0]}/`;
154
139
 
155
- // Local names the file imports from any `@symbiote-native/*` package (`FlatList`, `Animated`).
140
+ // Local names the file imports from any `@symbiote-native/*` package (`FlatList`, `Animated`)
156
141
  function symbioteImportsOf(descriptor) {
157
142
  const locals = new Set();
158
143
  for (const block of [descriptor.script, descriptor.scriptSetup]) {
@@ -161,7 +146,7 @@ function symbioteImportsOf(descriptor) {
161
146
  const program = babelParse(block.content, { sourceType: 'module', plugins }).program;
162
147
  for (const statement of program.body) {
163
148
  if (
164
- statement.type !== 'ImportDeclaration' ||
149
+ statement.type !== IMPORT_DECLARATION ||
165
150
  !statement.source.value.startsWith(SYMBIOTE_SOURCE_PREFIX)
166
151
  )
167
152
  continue;
@@ -171,9 +156,8 @@ function symbioteImportsOf(descriptor) {
171
156
  return locals;
172
157
  }
173
158
 
174
- // Vue casts a bare attribute to `true` only for a prop declared Boolean. Our tags and components
175
- // declare none, so `nested-scroll-enabled` reached Android as "" and the ViewManager threw
176
- // `String cannot be cast to Boolean`. The app's own components keep Vue's semantics.
159
+ // Vue casts a bare attribute to `true` only for a declared Boolean prop, and our tags declare none
160
+ // So `nested-scroll-enabled` reached Android as "" and threw `String cannot be cast to Boolean`
177
161
  function createBareBooleanNodeTransform(symbioteLocals) {
178
162
  const isOurs = tag =>
179
163
  INTRINSIC_TAGS.has(tag) || symbioteLocals.has(tag.split('.')[0]);
@@ -195,32 +179,26 @@ function createBareBooleanNodeTransform(symbioteLocals) {
195
179
  };
196
180
  }
197
181
 
198
-
199
- // A short, stable id per file, used as the SFC scope id regardless of whether the file has
200
- // scoped styles - built on css-parser's shared hashFilePath so the algorithm isn't duplicated
201
- // against the standalone .module.css compiler's identical need.
182
+ // A short, stable id per file, used as the SFC scope id whether or not it has scoped styles
183
+ // Built on css-parser's `hashFilePath`, shared with the standalone .module.css compiler
202
184
  function scopeIdFor(filename) {
203
185
  return 'data-v-' + hashFilePath(filename);
204
186
  }
205
187
 
206
- // The local the compiled component lands in when a <style module> block needs __cssModules hung
207
- // off it (compileScript's genDefaultAs). Same role as @vitejs/plugin-vue's own `_sfc_main`.
188
+ // The local the compiled component lands in when a <style module> block hangs `__cssModules` on it
189
+ // It is `genDefaultAs` of `compileScript`, the same role as plugin-vue's `_sfc_main`
208
190
  const SFC_COMPONENT_VAR = '__sfc__';
209
191
 
210
- // Blocks CONCATENATE - each is compiled on its own, so every block's rules restart at order 0 and
211
- // a later block would tie with an earlier one on the cascade's source-order tie-break, silently
212
- // reordering equally-specific rules. Renumbering on append gives the file one monotonic sequence,
213
- // which is what a single stylesheet holding the concatenated blocks would have produced.
192
+ // Each block restarts its rules at order 0, so a later one would tie an earlier one
193
+ // Renumbering on append gives the file one monotonic sequence, as one stylesheet would
214
194
  function appendRules(target, rules) {
215
195
  for (const rule of rules) {
216
196
  target.push({ ...rule, order: target.length });
217
197
  }
218
198
  }
219
199
 
220
- // `combinators` is compile-time-only - the registry matches a rule by token SUBSET and never reads
221
- // it - so it is stripped rather than shipped in every app bundle. Twin of css-parser's own
222
- // serializeRules (core/css-parser/src/metro-css-module/index.ts), which does this for a standalone
223
- // style file.
200
+ // `combinators` is compile-time-only, as the registry matches by token subset, so it is not shipped
201
+ // Twin of css-parser's `serializeRules` (metro-css-module/index.ts) for a standalone style file
224
202
  function serializeRules(rules) {
225
203
  return JSON.stringify(
226
204
  rules.map(({ tokens, specificity, order, style }) => ({
@@ -232,9 +210,8 @@ function serializeRules(rules) {
232
210
  );
233
211
  }
234
212
 
235
- // An SFC style block's `lang` attribute names a preprocessor language directly (`lang="scss"`),
236
- // unlike a standalone file, which is identified by its extension — so this is its own small
237
- // lookup rather than reusing detectLanguage(), which is extension-keyed.
213
+ // A style block's `lang` names a preprocessor (`lang="scss"`), where a file has an extension
214
+ // So this is its own small lookup, not `detectLanguage()`, which is extension-keyed
238
215
  const SFC_STYLE_LANG_TO_PREPROCESSOR = new Map([
239
216
  ['scss', 'scss'],
240
217
  ['sass', 'scss'],
@@ -242,10 +219,9 @@ const SFC_STYLE_LANG_TO_PREPROCESSOR = new Map([
242
219
  ['stylus', 'stylus'],
243
220
  ]);
244
221
 
245
- // Reduces one <style> block down to plain CSS text. A lang-less or lang="css" block passes
246
- // through unchanged. Anything outside the four recognized preprocessor langs throws.
222
+ // Reduces one <style> block to plain CSS: lang-less and `lang="css"` pass through, others compile
247
223
  async function compileStyleBlockContent(style, filename) {
248
- if (style.lang == null || style.lang === 'css') return style.content;
224
+ if (style.lang == null || style.lang === PLAIN_CSS_LANG) return style.content;
249
225
 
250
226
  const preprocessorLang = SFC_STYLE_LANG_TO_PREPROCESSOR.get(style.lang);
251
227
  if (!preprocessorLang) {
@@ -254,167 +230,146 @@ async function compileStyleBlockContent(style, filename) {
254
230
  );
255
231
  }
256
232
 
257
- // Sass' `.sass` indented syntax and `.scss` syntax share one compiler entry point that picks
258
- // between them off the file extension (see preprocessors.ts's compileScss) — an inline SFC
259
- // style block has no file of its own, so a synthetic `.sass`-suffixed path is the only way to
260
- // select the indented syntax. Every other preprocessor only uses the path for relative-import
261
- // resolution (dirname), where the real .vue file's own path is correct as-is.
233
+ // `.sass` and `.scss` share one compiler entry that picks the syntax by file extension
234
+ // An inline block has no file, so a synthetic `.sass` path selects the indented syntax
262
235
  const syntheticPath = style.lang === 'sass' ? `${filename}.sass` : filename;
263
236
  return compilePreprocessor(style.content, preprocessorLang, syntheticPath);
264
237
  }
265
238
 
266
- async function compileSfc(src, filename) {
267
- // `parse` memoizes its descriptor on (source, filename) in a module-global LRU, and the node
268
- // scope-class transform below MUTATES that descriptor's template AST in place, replacing
269
- // `prop.exp`. Handing the same object to a second compile therefore walks a half-transformed
270
- // tree and generation dies with `Codegen node is missing for element/if/for node`.
271
- //
272
- // Metro compiles each file once per build, so the cache buys this caller nothing; the only place
273
- // it was ever exercised is css-parser's golden-corpus determinism check, which compiles one
274
- // source twice on purpose. That is the ONLY test anywhere that can see this, which is why a
275
- // transform bug surfaced as a failure in another package's corpus.
276
- //
277
- // `clear()` rather than deleting our one key: the key is `source + JSON.stringify(options)`
278
- // (`@vue/shared`'s genCacheKey), so reproducing it here would couple this file to an internal
279
- // format across every Vue bump. Clearing a pure memo is always safe — the only cost is a
280
- // re-parse, which is what we need anyway.
281
-
239
+ // `parse` memoizes its descriptor and the scope-class transform mutates its AST in place
240
+ // A second compile of one source would walk a half-transformed tree and die in codegen
241
+ // `clear()` rather than deleting our key, as the key format is internal to `@vue/shared`
242
+ function parseDescriptor(src, filename) {
282
243
  parseCache.clear();
283
- // element-vs-component is decided by the PARSER, not the later `isCustomElement` in
284
- // templateOptions — `compileScript` reuses this descriptor's already-parsed AST, where `tagType`
285
- // is fixed. Without the answer here, `<pressable>` stays a component: `resolveComponent(...)`.
244
+ // The parser decides element vs component, and `compileScript` reuses that parsed AST
245
+ // Without `isCustomElement` here, `<pressable>` stays a component: `resolveComponent(...)`
286
246
  const { descriptor, errors } = parse(src, {
287
247
  filename,
288
- templateParseOptions: { isCustomElement: tag => INTRINSIC_TAGS.has(tag) },
248
+ templateParseOptions: { isCustomElement: isIntrinsicTag },
289
249
  });
290
250
  if (errors && errors.length > 0) {
291
251
  throw new Error(
292
252
  `Vue SFC parse error in ${filename}:\n${errors.map(String).join('\n')}`,
293
253
  );
294
254
  }
295
- if (descriptor.scriptSetup == null && descriptor.script == null) {
296
- throw new Error(
297
- `Vue SFC ${filename} has no <script> / <script setup> block`,
298
- );
255
+ if (descriptor.scriptSetup != null || descriptor.script != null) {
256
+ return descriptor;
299
257
  }
258
+ if (descriptor.template == null) {
259
+ throw new Error(`Vue SFC ${filename} has no <template> / <script> block`);
260
+ }
261
+ // `compileScript` refuses a file without a script, yet a static template is a valid component
262
+ // A `<script setup>` appended after the source keeps every line number of the file
263
+ // It carries a comment since the parser drops a blank block, which would recurse forever
264
+ return parseDescriptor(
265
+ `${src}\n<script setup>/* static template */</script>\n`,
266
+ filename,
267
+ );
268
+ }
300
269
 
301
- const scopeId = scopeIdFor(filename);
270
+ // `<style scoped>` and `<style module>` are one mechanism, lightningcss' CSS-Modules renaming
271
+ // Only the pattern differs, and a plain `<style>` is not renamed, so its classes register globally
302
272
 
303
- // descriptor.styles is already parsed by @vue/compiler-sfc (one entry per <style> block,
304
- // content pre-trimmed, scoped as a plain boolean) - no need to re-extract with a regex.
305
- //
306
- // Both scoped forms are ONE mechanism, lightningcss's CSS-Modules renaming, differing only in
307
- // the pattern string (core/css-parser/src/scoped-classes.ts):
308
- //
309
- // <style scoped> [local]__data-v-<hash> compileScopedCss - `.card` -> `card__data-v-h`
310
- // <style module> [local]__module__<hash> compileCssModule - the same call a standalone
311
- // .module.css takes, so the two cannot diverge
312
- // <style> not renamed at all compileCssToRules, classes register globally
313
- //
314
- // Renaming is our equivalent of Vue's `data-v-hash` ATTRIBUTE (we have no DOM and no
315
- // attribute-selector matching, so a scope can only be expressed in the name), and `:global(...)`
316
- // needs no handling here at all: a name lightningcss did not rename is global by definition.
317
- //
318
- // Multiple blocks cascade last-block-wins, same as CSS, after each block is scoped
319
- // independently — carried by the rules' `order`, which appendRules renumbers across blocks.
320
- //
321
- // A module block's classes are NEVER auto-applied to a literal class="..." (opt-in via $style.x
322
- // only), so they stay out of the template rewriter's name map; its output is the name->scopedName
323
- // map instead, emitted as a preamble const AND attached to the component as __cssModules.
273
+ // Renaming stands in for Vue's `data-v-hash` attribute, as there is no DOM to match one against
274
+ // `:global(...)` needs no handling here, since a name lightningcss did not rename is global
275
+ function compileSfcStyleBlock({ style, content, filename, scopeId }) {
276
+ if (style.module) {
277
+ const compiled = compileCssModule(content, filename);
278
+ const bindingName = typeof style.module === 'string' ? style.module : '$style';
279
+ return { rules: compiled.rules, binding: [bindingName, compiled.classMap] };
280
+ }
281
+ if (style.scoped) {
282
+ return compileScopedCss(content, {
283
+ filename,
284
+ pattern: `[local]__${scopeId}`,
285
+ });
286
+ }
287
+ return { rules: compileCssToRules(content, { filename }).rules };
288
+ }
289
+
290
+ // Blocks cascade last-wins like CSS, carried by the rules' `order` that `appendRules` renumbers
291
+ // A module block's classes never apply to a literal `class`, only through `$style.x`
292
+ async function compileStyles(descriptor, filename, scopeId) {
293
+ // A preprocessor block is reduced to plain CSS first, as the renaming only ever sees plain CSS
294
+ const contents = await Promise.all(
295
+ descriptor.styles.map(style => compileStyleBlockContent(style, filename)),
296
+ );
324
297
  const rules = [];
325
298
  const scopedClassNames = new Map();
326
299
  const cssModuleBindings = new Map();
327
-
328
- for (const style of descriptor.styles) {
329
- // Reduces a preprocessor block to plain CSS BEFORE the renaming below runs - the renaming is
330
- // language-agnostic, it only ever sees plain CSS.
331
- const content = await compileStyleBlockContent(style, filename);
332
-
333
- if (style.module) {
334
- const bindingName =
335
- typeof style.module === 'string' ? style.module : '$style';
336
- const compiled = compileCssModule(content, filename);
337
- appendRules(rules, compiled.rules);
300
+ descriptor.styles.forEach((style, index) => {
301
+ const block = compileSfcStyleBlock({
302
+ style,
303
+ content: contents[index],
304
+ filename,
305
+ scopeId,
306
+ });
307
+ appendRules(rules, block.rules);
308
+ for (const [name, renamed] of block.names ?? [])
309
+ scopedClassNames.set(name, renamed);
310
+ if (block.binding) {
311
+ const [bindingName, classMap] = block.binding;
338
312
  cssModuleBindings.set(bindingName, {
339
313
  ...cssModuleBindings.get(bindingName),
340
- ...compiled.classMap,
341
- });
342
- } else if (style.scoped) {
343
- const compiled = compileScopedCss(content, {
344
- filename,
345
- pattern: `[local]__${scopeId}`,
314
+ ...classMap,
346
315
  });
347
- appendRules(rules, compiled.rules);
348
- for (const [name, renamed] of compiled.names)
349
- scopedClassNames.set(name, renamed);
350
- } else {
351
- appendRules(rules, compileCssToRules(content, { filename }).rules);
352
316
  }
353
- }
317
+ });
318
+ return { rules, scopedClassNames, cssModuleBindings };
319
+ }
354
320
 
355
- // Scoped-class rewriting is skipped entirely (not even passed to the compiler) when nothing in
356
- // this file is scoped, so a .vue with only unscoped/no styles adds no runtime cost.
321
+ // Scoped-class rewriting is skipped when nothing in the file is scoped, so it adds no runtime cost
322
+ function templateOptionsFor(descriptor, scopedClassNames) {
357
323
  const nodeTransforms = [
358
324
  trimReflowedTextNodeTransform,
359
325
  createBareBooleanNodeTransform(symbioteImportsOf(descriptor)),
360
326
  ];
361
327
  if (scopedClassNames.size > 0)
362
328
  nodeTransforms.push(createScopeClassNodeTransform(scopedClassNames));
363
- // UNCONDITIONAL, and covering EVERY intrinsic rather than the ones this file happens to import:
364
- // a narrower `isCustomElement` leaves `<pressable>` compiling to `resolveComponent(...)` (blank
365
- // subtree, no error), and `<switch v-model>` a hard compile error (non-custom, non-form element).
366
- const templateOptions = {
367
- compilerOptions: {
368
- ...(nodeTransforms.length > 0 ? { nodeTransforms } : {}),
369
- isCustomElement: tag => INTRINSIC_TAGS.has(tag),
370
- },
371
- };
329
+ // Every intrinsic, not just the imported ones: a narrower list leaves `<pressable>` a blank
330
+ // `resolveComponent(...)` and makes `<switch v-model>` a compile error
331
+ return { compilerOptions: { nodeTransforms, isCustomElement: isIntrinsicTag } };
332
+ }
372
333
 
373
- // inlineTemplate folds the <template> render fn into setup(): one module, one `export
374
- // default`. Only valid with <script setup>, which the canary uses.
375
- //
376
- // genDefaultAs turns that `export default {...}` into `const __sfc__ = {...}`, so a <style
377
- // module> block can hang __cssModules off the component OPTIONS object before the module
378
- // exports it (the same thing @vitejs/plugin-vue does). Vue resolves a template's `$style` /
379
- // `classes` off `instance.type.__cssModules`, NOT off module scope, so without this the
380
- // emitted const is unreachable from the template and `$style.card` throws at render. Only
381
- // requested when a module block exists, so every other .vue file keeps identical output.
382
- const hasCssModules = cssModuleBindings.size > 0;
334
+ // `inlineTemplate` folds the render fn into `setup()`, giving one module and one export default
335
+ // `genDefaultAs` makes it a const so `__cssModules` can hang off it, as Vue reads `$style` there
336
+ function compileComponentScript(descriptor, scopeId, styles) {
337
+ const hasCssModules = styles.cssModuleBindings.size > 0;
383
338
  const compiled = compileScript(descriptor, {
384
339
  id: scopeId,
385
340
  inlineTemplate: true,
386
- templateOptions,
341
+ templateOptions: templateOptionsFor(descriptor, styles.scopedClassNames),
387
342
  fs: compileScriptFs,
388
343
  ...(hasCssModules ? { genDefaultAs: SFC_COMPONENT_VAR } : {}),
389
344
  });
390
- // Retargets every Vue import (compiler-injected helpers AND the user's own `from 'vue'`) at
391
- // the runtime-helpers shim - no vue/runtime-dom in a native bundle.
392
- const code = compiled.content.replace(
345
+ // Every Vue import, injected helpers and the user's own, goes to the shim: no runtime-dom
346
+ return compiled.content.replace(
393
347
  /from\s*(['"])vue\1/g,
394
348
  'from "@symbiote-native/vue/runtime-helpers"',
395
349
  );
350
+ }
396
351
 
352
+ function withStyleRegistration(code, { rules, scopedClassNames, cssModuleBindings }) {
353
+ const hasCssModules = cssModuleBindings.size > 0;
397
354
  if (rules.length === 0 && !hasCssModules) return code;
398
355
 
399
- // Only a scoped file needs the runtime rename helper and its per-file map, so these stay
400
- // unimported for every non-scoped .vue file.
401
- const engineImports =
402
- scopedClassNames.size > 0
403
- ? 'registerRules, renameClassTokens as __scopeClass'
404
- : 'registerRules';
356
+ // Only a scoped file needs the runtime rename helper and its per-file map
357
+ const isScoped = scopedClassNames.size > 0;
358
+ const engineImports = isScoped
359
+ ? 'registerRules, renameClassTokens as __scopeClass'
360
+ : 'registerRules';
405
361
 
406
362
  const preamble = [
407
363
  `import { ${engineImports} } from '@symbiote-native/engine';`,
408
364
  `registerRules(${serializeRules(rules)});`,
409
365
  ];
410
- if (scopedClassNames.size > 0) {
366
+ if (isScoped) {
411
367
  preamble.push(
412
368
  `const __scopedClassNames = ${JSON.stringify(Object.fromEntries(scopedClassNames))};`,
413
369
  );
414
370
  }
415
- // Each <style module> binding becomes a top-level const holding its name->scopedName map,
416
- // placed before the component so it's a closed-over module-scope variable usable from
417
- // <script setup> code itself; the __cssModules tail below is what makes the TEMPLATE see it.
371
+ // Each <style module> binding is a top-level const placed before the component, so <script setup>
372
+ // can close over it; the `__cssModules` tail below is what makes the TEMPLATE see it
418
373
  for (const [bindingName, classMap] of cssModuleBindings) {
419
374
  preamble.push(`const ${bindingName} = ${JSON.stringify(classMap)};`);
420
375
  }
@@ -433,39 +388,32 @@ async function compileSfc(src, filename) {
433
388
  return parts.join('\n') + '\n';
434
389
  }
435
390
 
391
+ async function compileSfc(src, filename) {
392
+ const descriptor = parseDescriptor(src, filename);
393
+ const scopeId = scopeIdFor(filename);
394
+ const styles = await compileStyles(descriptor, filename, scopeId);
395
+ const code = compileComponentScript(descriptor, scopeId, styles);
396
+ return withStyleRegistration(code, styles);
397
+ }
398
+
436
399
  // Exported separately from `transform` so tests can assert on the compiled SFC output
437
- // (imports, injected `registerRules` call) without driving the full upstream RN Babel preset.
400
+ // (imports, injected `registerRules` call) without driving RN's whole Babel preset
438
401
  module.exports.compileSfc = compileSfc;
439
402
 
440
- // Async uniformly, including branches that never touch a preprocessor: compileSfc() itself is
441
- // async now (a scss/sass/less/stylus block awaits preprocessors.ts's compile()), and Metro's own
442
- // metro-transform-worker already awaits transformer.transform(...), so this is a supported
443
- // shape - not worth forking a sync fast-path to save one microtask on a build-time,
444
- // content-hash-cached call.
403
+ // A standalone style file, not the inline `<style>` of a .vue, is compiled by this shared wrapper
404
+ const styleFileTransformer = createCssMetroTransformer(upstreamTransformer);
405
+
406
+ // Async throughout, as `compileSfc` awaits the style preprocessors and Metro awaits `transform`
445
407
  module.exports.transform = async function transform(params) {
446
408
  if (params.filename.endsWith('.vue')) {
447
409
  const code = await compileSfc(params.src, params.filename);
448
- // Re-label as .tsx so RN's transformer strips TS from <script setup lang="ts">. Metro
449
- // tracks the real path separately.
450
- return upstreamTransformer.transform({
451
- ...params,
452
- src: code,
453
- filename: params.filename + '.tsx',
454
- });
455
- }
456
- // A standalone style file (not a .vue file's inline <style> block) via the framework-agnostic
457
- // compileCssFile path. isStyleFile recognizes .css/.scss/.sass/.less/.styl/.stylus (+ .module.* twins).
458
- if (isStyleFile(params.filename)) {
459
- const { code } = await compileCssFile(params.src, params.filename);
460
- return upstreamTransformer.transform({
461
- ...params,
462
- src: code,
463
- filename: params.filename + '.js',
464
- });
410
+ // Re-label as .tsx so RN's transformer strips TS from `<script setup lang="ts">`
411
+ // Metro tracks the real path separately
412
+ return upstreamTransformer.transform(relabelForUpstream(params, code, '.tsx'));
465
413
  }
466
- return upstreamTransformer.transform(params);
414
+ return styleFileTransformer.transform(params);
467
415
  };
468
416
 
469
- // Surface the upstream cache key so RN preset changes still bust Metro's cache. The SFC
470
- // step itself is invalidated by `--reset-cache` (mirrors the babel DEBUG-inline note).
417
+ // Surface the upstream cache key so RN preset changes still bust Metro's cache
418
+ // The SFC step itself is invalidated by `--reset-cache`
471
419
  module.exports.getCacheKey = upstreamTransformer.getCacheKey;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/vue",
3
- "version": "3.3.0",
3
+ "version": "3.5.0",
4
4
  "description": "SymbioteNative's Vue adapter — a @vue/runtime-core createRenderer driving real native iOS/Android views through the same engine as the React adapter.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -70,18 +70,18 @@
70
70
  "@vue/compiler-sfc": "^3.5.13",
71
71
  "@vue/runtime-core": "^3.5.13",
72
72
  "typescript": "~6.0.0",
73
- "@symbiote-native/components": "3.2.0",
74
- "@symbiote-native/css-parser": "0.5.4"
73
+ "@symbiote-native/components": "3.3.0",
74
+ "@symbiote-native/css-parser": "0.6.0"
75
75
  },
76
76
  "peerDependencies": {
77
77
  "vue": ">=3.5.0",
78
78
  "react-native": ">=0.86",
79
- "@symbiote-native/engine": "^1.6.0"
79
+ "@symbiote-native/engine": "^1.7.0"
80
80
  },
81
81
  "devDependencies": {
82
82
  "@babel/core": "^7.25.2",
83
- "@symbiote-native/engine": "1.6.0",
84
- "@symbiote-native/test-utils": "0.4.7"
83
+ "@symbiote-native/engine": "1.7.0",
84
+ "@symbiote-native/test-utils": "0.5.0"
85
85
  },
86
86
  "scripts": {
87
87
  "typecheck": "tsc --build",