@symbiote-native/vue 3.2.0 → 3.4.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 (59) hide show
  1. package/build/components/checkbox-props.d.ts +3 -0
  2. package/build/components/checkbox-props.js +1 -0
  3. package/build/components/flat-list/index.d.ts +4 -3
  4. package/build/components/flat-list/index.js +18 -21
  5. package/build/components/input-accessory-view-props.d.ts +4 -4
  6. package/build/components/keyboard-avoiding-view.d.ts +11 -3
  7. package/build/components/keyboard-avoiding-view.js +38 -69
  8. package/build/components/layout-conformance-props.d.ts +4 -0
  9. package/build/components/layout-conformance-props.js +1 -0
  10. package/build/components/modal/index.d.ts +18 -5
  11. package/build/components/modal/index.js +53 -24
  12. package/build/components/refresh-control-props.d.ts +7 -7
  13. package/build/components/scroll-view/scroll-view-props.d.ts +8 -5
  14. package/build/components/section-list/index.d.ts +1 -1
  15. package/build/components/section-list/index.js +2 -6
  16. package/build/components/text-props.d.ts +3 -3
  17. package/build/components/touchable-props.d.ts +6 -6
  18. package/build/components/virtualized-list/index.d.ts +5 -75
  19. package/build/components/virtualized-list/index.js +130 -558
  20. package/build/components/virtualized-list/list-children.d.ts +18 -0
  21. package/build/components/virtualized-list/list-children.js +81 -0
  22. package/build/components/virtualized-list/list-elements.d.ts +7 -0
  23. package/build/components/virtualized-list/list-elements.js +29 -0
  24. package/build/components/virtualized-list/list-scroll-props.d.ts +20 -0
  25. package/build/components/virtualized-list/list-scroll-props.js +9 -0
  26. package/build/components/virtualized-list/narrow-props.d.ts +79 -0
  27. package/build/components/virtualized-list/narrow-props.js +89 -0
  28. package/build/components/virtualized-list/nested-scope.d.ts +3 -0
  29. package/build/components/virtualized-list/nested-scope.js +12 -0
  30. package/build/components/virtualized-list/user-handlers.d.ts +3 -0
  31. package/build/components/virtualized-list/user-handlers.js +24 -0
  32. package/build/components/virtualized-list/virtualized-list-props.d.ts +82 -0
  33. package/build/components/virtualized-list/virtualized-list-props.js +41 -0
  34. package/build/components/virtualized-section-list/index.d.ts +20 -5
  35. package/build/components/virtualized-section-list/index.js +81 -95
  36. package/build/components/virtualized-section-list/section-item-cell.d.ts +18 -0
  37. package/build/components/virtualized-section-list/section-item-cell.js +52 -0
  38. package/build/composables/define-native-view-component.d.ts +3 -0
  39. package/build/composables/define-native-view-component.js +23 -0
  40. package/build/composables/use-animated-value.d.ts +7 -0
  41. package/build/composables/use-animated-value.js +14 -0
  42. package/build/composables/use-color-scheme.d.ts +1 -1
  43. package/build/composables/use-event.d.ts +6 -0
  44. package/build/composables/use-event.js +22 -0
  45. package/build/create-transition/index.d.ts +2 -2
  46. package/build/create-transition/index.js +1 -1
  47. package/build/descriptor-to-vue.d.ts +1 -1
  48. package/build/descriptor-to-vue.js +6 -2
  49. package/build/index.d.ts +16 -4
  50. package/build/index.js +18 -31
  51. package/build/jsx-runtime.d.ts +2 -0
  52. package/build/modules/animated/index.d.ts +54 -41
  53. package/build/modules/animated/index.js +2 -26
  54. package/build/modules/app-registry/index.d.ts +3 -3
  55. package/build/modules/app-registry/index.js +2 -2
  56. package/build/modules/status-bar.js +22 -24
  57. package/build/utils/normalize-attrs.js +10 -4
  58. package/metro-vue-transformer.cjs +165 -224
  59. 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,9 @@ 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
137
  const SYMBIOTE_SOURCE_PREFIX = '@symbiote-native/';
154
138
 
155
- // Local names the file imports from any `@symbiote-native/*` package (`FlatList`, `Animated`).
139
+ // Local names the file imports from any `@symbiote-native/*` package (`FlatList`, `Animated`)
156
140
  function symbioteImportsOf(descriptor) {
157
141
  const locals = new Set();
158
142
  for (const block of [descriptor.script, descriptor.scriptSetup]) {
@@ -161,7 +145,7 @@ function symbioteImportsOf(descriptor) {
161
145
  const program = babelParse(block.content, { sourceType: 'module', plugins }).program;
162
146
  for (const statement of program.body) {
163
147
  if (
164
- statement.type !== 'ImportDeclaration' ||
148
+ statement.type !== IMPORT_DECLARATION ||
165
149
  !statement.source.value.startsWith(SYMBIOTE_SOURCE_PREFIX)
166
150
  )
167
151
  continue;
@@ -171,9 +155,8 @@ function symbioteImportsOf(descriptor) {
171
155
  return locals;
172
156
  }
173
157
 
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.
158
+ // Vue casts a bare attribute to `true` only for a declared Boolean prop, and our tags declare none
159
+ // So `nested-scroll-enabled` reached Android as "" and threw `String cannot be cast to Boolean`
177
160
  function createBareBooleanNodeTransform(symbioteLocals) {
178
161
  const isOurs = tag =>
179
162
  INTRINSIC_TAGS.has(tag) || symbioteLocals.has(tag.split('.')[0]);
@@ -195,32 +178,26 @@ function createBareBooleanNodeTransform(symbioteLocals) {
195
178
  };
196
179
  }
197
180
 
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.
181
+ // A short, stable id per file, used as the SFC scope id whether or not it has scoped styles
182
+ // Built on css-parser's `hashFilePath`, shared with the standalone .module.css compiler
202
183
  function scopeIdFor(filename) {
203
184
  return 'data-v-' + hashFilePath(filename);
204
185
  }
205
186
 
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`.
187
+ // The local the compiled component lands in when a <style module> block hangs `__cssModules` on it
188
+ // It is `genDefaultAs` of `compileScript`, the same role as plugin-vue's `_sfc_main`
208
189
  const SFC_COMPONENT_VAR = '__sfc__';
209
190
 
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.
191
+ // Each block restarts its rules at order 0, so a later one would tie an earlier one
192
+ // Renumbering on append gives the file one monotonic sequence, as one stylesheet would
214
193
  function appendRules(target, rules) {
215
194
  for (const rule of rules) {
216
195
  target.push({ ...rule, order: target.length });
217
196
  }
218
197
  }
219
198
 
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.
199
+ // `combinators` is compile-time-only, as the registry matches by token subset, so it is not shipped
200
+ // Twin of css-parser's `serializeRules` (metro-css-module/index.ts) for a standalone style file
224
201
  function serializeRules(rules) {
225
202
  return JSON.stringify(
226
203
  rules.map(({ tokens, specificity, order, style }) => ({
@@ -232,9 +209,8 @@ function serializeRules(rules) {
232
209
  );
233
210
  }
234
211
 
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.
212
+ // A style block's `lang` names a preprocessor (`lang="scss"`), where a file has an extension
213
+ // So this is its own small lookup, not `detectLanguage()`, which is extension-keyed
238
214
  const SFC_STYLE_LANG_TO_PREPROCESSOR = new Map([
239
215
  ['scss', 'scss'],
240
216
  ['sass', 'scss'],
@@ -242,10 +218,9 @@ const SFC_STYLE_LANG_TO_PREPROCESSOR = new Map([
242
218
  ['stylus', 'stylus'],
243
219
  ]);
244
220
 
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.
221
+ // Reduces one <style> block to plain CSS: lang-less and `lang="css"` pass through, others compile
247
222
  async function compileStyleBlockContent(style, filename) {
248
- if (style.lang == null || style.lang === 'css') return style.content;
223
+ if (style.lang == null || style.lang === PLAIN_CSS_LANG) return style.content;
249
224
 
250
225
  const preprocessorLang = SFC_STYLE_LANG_TO_PREPROCESSOR.get(style.lang);
251
226
  if (!preprocessorLang) {
@@ -254,38 +229,22 @@ async function compileStyleBlockContent(style, filename) {
254
229
  );
255
230
  }
256
231
 
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.
232
+ // `.sass` and `.scss` share one compiler entry that picks the syntax by file extension
233
+ // An inline block has no file, so a synthetic `.sass` path selects the indented syntax
262
234
  const syntheticPath = style.lang === 'sass' ? `${filename}.sass` : filename;
263
235
  return compilePreprocessor(style.content, preprocessorLang, syntheticPath);
264
236
  }
265
237
 
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
-
238
+ // `parse` memoizes its descriptor and the scope-class transform mutates its AST in place
239
+ // A second compile of one source would walk a half-transformed tree and die in codegen
240
+ // `clear()` rather than deleting our key, as the key format is internal to `@vue/shared`
241
+ function parseDescriptor(src, filename) {
282
242
  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(...)`.
243
+ // The parser decides element vs component, and `compileScript` reuses that parsed AST
244
+ // Without `isCustomElement` here, `<pressable>` stays a component: `resolveComponent(...)`
286
245
  const { descriptor, errors } = parse(src, {
287
246
  filename,
288
- templateParseOptions: { isCustomElement: tag => INTRINSIC_TAGS.has(tag) },
247
+ templateParseOptions: { isCustomElement: isIntrinsicTag },
289
248
  });
290
249
  if (errors && errors.length > 0) {
291
250
  throw new Error(
@@ -297,124 +256,113 @@ async function compileSfc(src, filename) {
297
256
  `Vue SFC ${filename} has no <script> / <script setup> block`,
298
257
  );
299
258
  }
259
+ return descriptor;
260
+ }
300
261
 
301
- const scopeId = scopeIdFor(filename);
262
+ // `<style scoped>` and `<style module>` are one mechanism, lightningcss' CSS-Modules renaming
263
+ // Only the pattern differs, and a plain `<style>` is not renamed, so its classes register globally
264
+
265
+ // Renaming stands in for Vue's `data-v-hash` attribute, as there is no DOM to match one against
266
+ // `:global(...)` needs no handling here, since a name lightningcss did not rename is global
267
+ function compileSfcStyleBlock({ style, content, filename, scopeId }) {
268
+ if (style.module) {
269
+ const compiled = compileCssModule(content, filename);
270
+ const bindingName = typeof style.module === 'string' ? style.module : '$style';
271
+ return { rules: compiled.rules, binding: [bindingName, compiled.classMap] };
272
+ }
273
+ if (style.scoped) {
274
+ const compiled = compileScopedCss(content, {
275
+ filename,
276
+ pattern: `[local]__${scopeId}`,
277
+ });
278
+ return { rules: compiled.rules, names: compiled.names };
279
+ }
280
+ return { rules: compileCssToRules(content, { filename }).rules };
281
+ }
302
282
 
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.
283
+ // Blocks cascade last-wins like CSS, carried by the rules' `order` that `appendRules` renumbers
284
+ // A module block's classes never apply to a literal `class`, only through `$style.x`
285
+ async function compileStyles(descriptor, filename, scopeId) {
286
+ // A preprocessor block is reduced to plain CSS first, as the renaming only ever sees plain CSS
287
+ const contents = await Promise.all(
288
+ descriptor.styles.map(style => compileStyleBlockContent(style, filename)),
289
+ );
324
290
  const rules = [];
325
291
  const scopedClassNames = new Map();
326
292
  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);
293
+ descriptor.styles.forEach((style, index) => {
294
+ const block = compileSfcStyleBlock({
295
+ style,
296
+ content: contents[index],
297
+ filename,
298
+ scopeId,
299
+ });
300
+ appendRules(rules, block.rules);
301
+ for (const [name, renamed] of block.names ?? [])
302
+ scopedClassNames.set(name, renamed);
303
+ if (block.binding) {
304
+ const [bindingName, classMap] = block.binding;
338
305
  cssModuleBindings.set(bindingName, {
339
306
  ...cssModuleBindings.get(bindingName),
340
- ...compiled.classMap,
341
- });
342
- } else if (style.scoped) {
343
- const compiled = compileScopedCss(content, {
344
- filename,
345
- pattern: `[local]__${scopeId}`,
307
+ ...classMap,
346
308
  });
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
309
  }
353
- }
310
+ });
311
+ return { rules, scopedClassNames, cssModuleBindings };
312
+ }
354
313
 
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.
314
+ // Scoped-class rewriting is skipped when nothing in the file is scoped, so it adds no runtime cost
315
+ function templateOptionsFor(descriptor, scopedClassNames) {
357
316
  const nodeTransforms = [
358
317
  trimReflowedTextNodeTransform,
359
318
  createBareBooleanNodeTransform(symbioteImportsOf(descriptor)),
360
319
  ];
361
320
  if (scopedClassNames.size > 0)
362
321
  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
- };
322
+ // Every intrinsic, not just the imported ones: a narrower list leaves `<pressable>` a blank
323
+ // `resolveComponent(...)` and makes `<switch v-model>` a compile error
324
+ return { compilerOptions: { nodeTransforms, isCustomElement: isIntrinsicTag } };
325
+ }
372
326
 
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;
327
+ // `inlineTemplate` folds the render fn into `setup()`, giving one module and one export default
328
+ // `genDefaultAs` makes it a const so `__cssModules` can hang off it, as Vue reads `$style` there
329
+ function compileComponentScript(descriptor, scopeId, styles) {
330
+ const hasCssModules = styles.cssModuleBindings.size > 0;
383
331
  const compiled = compileScript(descriptor, {
384
332
  id: scopeId,
385
333
  inlineTemplate: true,
386
- templateOptions,
334
+ templateOptions: templateOptionsFor(descriptor, styles.scopedClassNames),
387
335
  fs: compileScriptFs,
388
336
  ...(hasCssModules ? { genDefaultAs: SFC_COMPONENT_VAR } : {}),
389
337
  });
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(
338
+ // Every Vue import, injected helpers and the user's own, goes to the shim: no runtime-dom
339
+ return compiled.content.replace(
393
340
  /from\s*(['"])vue\1/g,
394
341
  'from "@symbiote-native/vue/runtime-helpers"',
395
342
  );
343
+ }
396
344
 
345
+ function withStyleRegistration(code, { rules, scopedClassNames, cssModuleBindings }) {
346
+ const hasCssModules = cssModuleBindings.size > 0;
397
347
  if (rules.length === 0 && !hasCssModules) return code;
398
348
 
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';
349
+ // Only a scoped file needs the runtime rename helper and its per-file map
350
+ const isScoped = scopedClassNames.size > 0;
351
+ const engineImports = isScoped
352
+ ? 'registerRules, renameClassTokens as __scopeClass'
353
+ : 'registerRules';
405
354
 
406
355
  const preamble = [
407
356
  `import { ${engineImports} } from '@symbiote-native/engine';`,
408
357
  `registerRules(${serializeRules(rules)});`,
409
358
  ];
410
- if (scopedClassNames.size > 0) {
359
+ if (isScoped) {
411
360
  preamble.push(
412
361
  `const __scopedClassNames = ${JSON.stringify(Object.fromEntries(scopedClassNames))};`,
413
362
  );
414
363
  }
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.
364
+ // Each <style module> binding is a top-level const placed before the component, so <script setup>
365
+ // can close over it; the `__cssModules` tail below is what makes the TEMPLATE see it
418
366
  for (const [bindingName, classMap] of cssModuleBindings) {
419
367
  preamble.push(`const ${bindingName} = ${JSON.stringify(classMap)};`);
420
368
  }
@@ -433,39 +381,32 @@ async function compileSfc(src, filename) {
433
381
  return parts.join('\n') + '\n';
434
382
  }
435
383
 
384
+ async function compileSfc(src, filename) {
385
+ const descriptor = parseDescriptor(src, filename);
386
+ const scopeId = scopeIdFor(filename);
387
+ const styles = await compileStyles(descriptor, filename, scopeId);
388
+ const code = compileComponentScript(descriptor, scopeId, styles);
389
+ return withStyleRegistration(code, styles);
390
+ }
391
+
436
392
  // 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.
393
+ // (imports, injected `registerRules` call) without driving RN's whole Babel preset
438
394
  module.exports.compileSfc = compileSfc;
439
395
 
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.
396
+ // A standalone style file, not the inline `<style>` of a .vue, is compiled by this shared wrapper
397
+ const styleFileTransformer = createCssMetroTransformer(upstreamTransformer);
398
+
399
+ // Async throughout, as `compileSfc` awaits the style preprocessors and Metro awaits `transform`
445
400
  module.exports.transform = async function transform(params) {
446
401
  if (params.filename.endsWith('.vue')) {
447
402
  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
- });
403
+ // Re-label as .tsx so RN's transformer strips TS from `<script setup lang="ts">`
404
+ // Metro tracks the real path separately
405
+ return upstreamTransformer.transform(relabelForUpstream(params, code, '.tsx'));
465
406
  }
466
- return upstreamTransformer.transform(params);
407
+ return styleFileTransformer.transform(params);
467
408
  };
468
409
 
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).
410
+ // Surface the upstream cache key so RN preset changes still bust Metro's cache
411
+ // The SFC step itself is invalidated by `--reset-cache`
471
412
  module.exports.getCacheKey = upstreamTransformer.getCacheKey;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/vue",
3
- "version": "3.2.0",
3
+ "version": "3.4.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.1.4",
74
- "@symbiote-native/css-parser": "0.5.3"
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.5.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.5.0",
84
- "@symbiote-native/test-utils": "0.4.6"
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",