@octane-xplat/patches 0.14.0 → 0.16.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octane-xplat/patches",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "pnpm config dependency carrying the framework patch files",
5
5
  "keywords": [
6
6
  "framework",
@@ -1,20 +1,195 @@
1
1
  diff --git a/dist/driver.js b/dist/driver.js
2
- index c88828e30337ee566bc1a919940e58b4b0227aba..52cde3f8f8acffcc05a1db8e0fcd90515efc70f6 100644
2
+ index c88828e30337ee566bc1a919940e58b4b0227aba..5fcee7ffb92d8ddf937b65b27825cbb81944a195 100644
3
3
  --- a/dist/driver.js
4
4
  +++ b/dist/driver.js
5
- @@ -85,6 +85,11 @@ function setProp(node, view, name, value) {
5
+ @@ -56,15 +56,34 @@ function applyProps(node, props) {
6
+ }
7
+ }
8
+ /**
9
+ - * `update` carries the host's dynamic-prop snapshot; attributes that were
10
+ - * static in the compiled plan arrive only with `create`. Merge instead of
11
+ - * replace, or a first update would strip a node's static layout props.
12
+ + * `update` carries the host's complete prop bag: a key present in the last
13
+ + * applied bag but absent from the next is a removal, and the view state it
14
+ + * installed returns to the prop's documented default. `undefined` never
15
+ + * crosses the wire (the runtime elides it); `null` is "not supplied" for
16
+ + * props without an explicit nullable contract — both reset through setProp.
17
+ */
18
+ function updateProps(node, next) {
19
+ + const dropped = [];
20
+ + for (const name in node.props) {
21
+ + if (node.props[name] != null && !(name in next))
22
+ + dropped.push(name);
23
+ + }
24
+ applyProps(node, next);
25
+ - node.props = { ...node.props, ...next };
26
+ + for (const name of dropped) {
27
+ + resetProp(node, node.view, name);
28
+ + }
29
+ + node.props = next;
30
+ }
31
+ function setProp(node, view, name, value) {
32
+ + // Keep authored placement separate from Grid's computed native coordinates.
33
+ + // NativeScript does not retain a setter write equal to its default (row/col 0).
34
+ + if (name === 'row' || name === 'col' || name === 'rowSpan' || name === 'colSpan') {
35
+ + const key = Symbol.for('octane-xplat.grid-placement');
36
+ + const placement = view[key] ?? (view[key] = {});
37
+ + if (value == null || value === unsetValue) delete placement[name];
38
+ + else placement[name] = value;
39
+ + view.notify({ eventName: 'xplatGridPlacementChange', object: view });
40
+ + }
41
+ if (name === 'children' || name === 'key' || name === 'ref')
42
+ return;
43
+ // Event props arrive as their own `event` commands.
44
+ @@ -78,13 +97,28 @@ function setProp(node, view, name, value) {
45
+ return;
46
+ }
47
+ if (name === 'style') {
48
+ - applyStyle(view, value);
49
+ + applyStyle(node, view, value);
50
+ return;
51
+ }
52
+ if (name === 'renderItem' && view instanceof ListView) {
6
53
  setRenderItem(view, value ?? null, createNativeScriptRoot);
7
54
  return;
8
55
  }
9
- + // `undefined` means "prop not supplied" — skip the write. NativeScript
10
- + // native setters coerce it (e.g. editable → ios.userInteractionEnabled =
11
- + // NO) leaving dead UI that still has its JS listeners.
12
- + if (value === undefined)
56
+ + // `null`/`unsetValue` restore the prop's default rather than writing a
57
+ + // literal null through a native setter. `undefined` is a removal: skip it
58
+ + // when nothing applied (NativeScript setters coerce it badly) and reset
59
+ + // when a real value did.
60
+ + if (value == null || value === unsetValue) {
61
+ + if (value === undefined && node.props[name] == null)
62
+ + return;
63
+ + resetProp(node, view, name);
13
64
  + return;
65
+ + }
66
+ + // Capture the pre-prop value once so a plain (non-Property) field can be
67
+ + // restored when the prop is removed later.
68
+ + const defaults = (node.propDefaults ??= {});
69
+ + if (!(name in defaults))
70
+ + defaults[name] = view[name];
14
71
  // Core raises `<name>Change` for a script write exactly as for a user
15
72
  // edit; a controlled input must not see its own value come back as one.
16
73
  const echo = `${name}Change`;
17
- @@ -482,7 +487,7 @@ function applyCommand(container, command) {
74
+ @@ -99,6 +133,44 @@ function setProp(node, view, name, value) {
75
+ node.muted.delete(echo);
76
+ }
77
+ }
78
+ +/** Restore a removed prop to the value it holds when never supplied. */
79
+ +function resetProp(node, view, name) {
80
+ + if (view === null)
81
+ + return;
82
+ + if (name === 'children' || name === 'key' || name === 'ref')
83
+ + return;
84
+ + // `on*` handlers unbind through `event` commands, not props.
85
+ + if (EVENT_PROP.test(name))
86
+ + return;
87
+ + if (name === 'className' || name === 'class') {
88
+ + view.className = unsetValue;
89
+ + return;
90
+ + }
91
+ + if (name === 'style') {
92
+ + applyStyle(node, view, null);
93
+ + return;
94
+ + }
95
+ + if (name === 'renderItem' && view instanceof ListView) {
96
+ + setRenderItem(view, null, createNativeScriptRoot);
97
+ + return;
98
+ + }
99
+ + const echo = `${name}Change`;
100
+ + const listening = node.listeners.has(echo);
101
+ + if (listening)
102
+ + node.muted.add(echo);
103
+ + try {
104
+ + // Property/CssProperty setters consume the sentinel and restore the
105
+ + // declared default. A plain field stores it raw — detect that and
106
+ + // write back the captured pre-prop value instead.
107
+ + view[name] = unsetValue;
108
+ + if (view[name] === unsetValue)
109
+ + view[name] = node.propDefaults?.[name];
110
+ + }
111
+ + finally {
112
+ + if (listening)
113
+ + node.muted.delete(echo);
114
+ + }
115
+ +}
116
+ // `class`/`className` compose clsx-style on the DOM renderer; join arrays
117
+ // with spaces so NativeScript's class selectors can match each class.
118
+ function normalizeClass(value) {
119
+ @@ -112,15 +184,67 @@ function normalizeClass(value) {
120
+ .join(' ');
121
+ return '';
122
+ }
123
+ -function applyStyle(view, value) {
124
+ - if (value == null || value === unsetValue)
125
+ - return;
126
+ +/**
127
+ + * The `style` prop replaces wholesale — a key dropped or nulled since the last
128
+ + * apply resets what it installed (`unsetValue` clears a local CssProperty
129
+ + * value so the cascade can re-apply). String styles are parsed for their
130
+ + * declared names so a later object or removal can reset them.
131
+ + */
132
+ +function applyStyle(node, view, value) {
133
+ + const applied = node.styleKeys;
134
+ + node.styleKeys = undefined;
135
+ if (typeof value === 'string') {
136
+ + clearStyleKeys(view, applied);
137
+ view.setInlineStyle(value);
138
+ + node.styleKeys = inlineStyleKeys(value);
139
+ + return;
140
+ + }
141
+ + if (value == null || value === unsetValue || typeof value !== 'object') {
142
+ + clearStyleKeys(view, applied);
143
+ return;
144
+ }
145
+ - if (typeof value === 'object')
146
+ - Object.assign(view.style, value);
147
+ + if (applied !== undefined) {
148
+ + for (const key of applied) {
149
+ + if (!(key in value))
150
+ + unsetStyleKey(view, key);
151
+ + }
152
+ + }
153
+ + for (const [key, nextValue] of Object.entries(value)) {
154
+ + if (nextValue == null)
155
+ + unsetStyleKey(view, key);
156
+ + else
157
+ + view.style[key] = nextValue;
158
+ + }
159
+ + node.styleKeys = new Set(Object.keys(value));
160
+ +}
161
+ +function clearStyleKeys(view, keys) {
162
+ + if (keys === undefined)
163
+ + return;
164
+ + for (const key of keys)
165
+ + unsetStyleKey(view, key);
166
+ +}
167
+ +function unsetStyleKey(view, name) {
168
+ + const style = view.style;
169
+ + if (name.startsWith('--')) {
170
+ + style.setUnscopedCssVariable?.(name, unsetValue);
171
+ + return;
172
+ + }
173
+ + style[name] = unsetValue;
174
+ + // A style field outside the CssProperty registry stores the sentinel raw.
175
+ + if (style[name] === unsetValue)
176
+ + style[name] = undefined;
177
+ +}
178
+ +function inlineStyleKeys(style) {
179
+ + const keys = new Set();
180
+ + for (const declaration of style.split(';')) {
181
+ + const colon = declaration.indexOf(':');
182
+ + if (colon === -1)
183
+ + continue;
184
+ + const name = declaration.slice(0, colon).trim();
185
+ + if (name !== '')
186
+ + keys.add(name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()));
187
+ + }
188
+ + return keys;
189
+ }
190
+ /** Recompute a text host's `text` from its `#text` children. */
191
+ function syncText(parent) {
192
+ @@ -482,7 +606,7 @@ function applyCommand(container, command) {
18
193
  }
19
194
  export const nativeScriptDriver = {
20
195
  id: NATIVESCRIPT_RENDERER_ID,
@@ -0,0 +1,620 @@
1
+ diff --git a/dist/driver.js b/dist/driver.js
2
+ index 8bc563a..4cb1f95 100644
3
+ --- a/dist/driver.js
4
+ +++ b/dist/driver.js
5
+ @@ -3,6 +3,7 @@ import { createUniversalRoot, } from 'octane/universal/native';
6
+ import { NATIVESCRIPT_RENDERER_ID } from './config.js';
7
+ import { ELEMENTS, EVENT_PROP, eventNameFor, onElementReplaced, } from './elements.js';
8
+ import { releaseListView, setRenderItem, } from './list-view.js';
9
+ +import { Style as MasonStyle } from '@triniwiz/nativescript-masonkit/style';
10
+ /** `#text` nodes carry no view; the driver folds them into the parent's `text`. */
11
+ const TEXT = '#text';
12
+ /**
13
+ @@ -73,7 +74,10 @@ function instantiate(type) {
14
+ if (Element === undefined) {
15
+ throw new Error(`NativeScript driver: <${type}> is not a registered element.`);
16
+ }
17
+ - return new Element();
18
+ + const view = new Element();
19
+ + if (MASON_CONTAINER_TAGS.has(type))
20
+ + applyMasonDefaults(type, view);
21
+ + return view;
22
+ }
23
+ function applyProps(node, props) {
24
+ if (node.view === null) {
25
+ @@ -91,13 +95,23 @@ function applyProps(node, props) {
26
+ }
27
+ }
28
+ /**
29
+ - * `update` carries the host's dynamic-prop snapshot; attributes that were
30
+ - * static in the compiled plan arrive only with `create`. Merge instead of
31
+ - * replace, or a first update would strip a node's static layout props.
32
+ + * `update` carries the host's complete prop bag: a key present in the last
33
+ + * applied bag but absent from the next is a removal, and the view state it
34
+ + * installed returns to the prop's documented default. `undefined` never
35
+ + * crosses the wire (the runtime elides it); `null` is "not supplied" for
36
+ + * props without an explicit nullable contract — both reset through setProp.
37
+ */
38
+ function updateProps(node, next) {
39
+ + const dropped = [];
40
+ + for (const name in node.props) {
41
+ + if (node.props[name] != null && !(name in next))
42
+ + dropped.push(name);
43
+ + }
44
+ applyProps(node, next);
45
+ - node.props = { ...node.props, ...next };
46
+ + for (const name of dropped) {
47
+ + resetProp(node, node.view, name);
48
+ + }
49
+ + node.props = next;
50
+ }
51
+ function setProp(node, view, name, value) {
52
+ if (name === 'children' || name === 'key' || name === 'ref')
53
+ @@ -113,13 +127,36 @@ function setProp(node, view, name, value) {
54
+ return;
55
+ }
56
+ if (name === 'style') {
57
+ - applyStyle(view, value);
58
+ + applyStyle(node, view, value);
59
+ return;
60
+ }
61
+ if (name === 'renderItem' && view instanceof ListView) {
62
+ setRenderItem(view, value ?? null, createNativeScriptRoot);
63
+ return;
64
+ }
65
+ + // Mason cutover — all layout props are style-only: warn when a former
66
+ + // prop name arrives on a mason container or a child of one.
67
+ + if (MASON_CONTAINER_TAGS.has(node.type))
68
+ + applyMasonContainerProp(node, view, name, value);
69
+ + if (MASON_STYLE_ONLY_PROPS.has(name) &&
70
+ + (MASON_CONTAINER_TAGS.has(node.type) ||
71
+ + (node.parent !== null && MASON_CONTAINER_TAGS.has(node.parent.type))))
72
+ + warnStyleOnlyProp(node, name);
73
+ + // `null`/`unsetValue` restore the prop's default rather than writing a
74
+ + // literal null through a native setter. `undefined` is a removal: skip it
75
+ + // when nothing applied (NativeScript setters coerce it badly) and reset
76
+ + // when a real value did.
77
+ + if (value == null || value === unsetValue) {
78
+ + if (value === undefined && node.props[name] == null)
79
+ + return;
80
+ + resetProp(node, view, name);
81
+ + return;
82
+ + }
83
+ + // Capture the pre-prop value once so a plain (non-Property) field can be
84
+ + // restored when the prop is removed later.
85
+ + const defaults = (node.propDefaults ??= {});
86
+ + if (!(name in defaults))
87
+ + defaults[name] = view[name];
88
+ // Core raises `<name>Change` for a script write exactly as for a user
89
+ // edit; a controlled input must not see its own value come back as one.
90
+ const echo = `${name}Change`;
91
+ @@ -134,6 +171,44 @@ function setProp(node, view, name, value) {
92
+ node.muted.delete(echo);
93
+ }
94
+ }
95
+ +/** Restore a removed prop to the value it holds when never supplied. */
96
+ +function resetProp(node, view, name) {
97
+ + if (view === null)
98
+ + return;
99
+ + if (name === 'children' || name === 'key' || name === 'ref')
100
+ + return;
101
+ + // `on*` handlers unbind through `event` commands, not props.
102
+ + if (EVENT_PROP.test(name))
103
+ + return;
104
+ + if (name === 'className' || name === 'class') {
105
+ + view.className = unsetValue;
106
+ + return;
107
+ + }
108
+ + if (name === 'style') {
109
+ + applyStyle(node, view, null);
110
+ + return;
111
+ + }
112
+ + if (name === 'renderItem' && view instanceof ListView) {
113
+ + setRenderItem(view, null, createNativeScriptRoot);
114
+ + return;
115
+ + }
116
+ + const echo = `${name}Change`;
117
+ + const listening = node.listeners.has(echo);
118
+ + if (listening)
119
+ + node.muted.add(echo);
120
+ + try {
121
+ + // Property/CssProperty setters consume the sentinel and restore the
122
+ + // declared default. A plain field stores it raw — detect that and
123
+ + // write back the captured pre-prop value instead.
124
+ + view[name] = unsetValue;
125
+ + if (view[name] === unsetValue)
126
+ + view[name] = node.propDefaults?.[name];
127
+ + }
128
+ + finally {
129
+ + if (listening)
130
+ + node.muted.delete(echo);
131
+ + }
132
+ +}
133
+ // `class`/`className` compose clsx-style on the DOM renderer; join arrays
134
+ // with spaces so NativeScript's class selectors can match each class.
135
+ function normalizeClass(value) {
136
+ @@ -147,16 +222,214 @@ function normalizeClass(value) {
137
+ .join(' ');
138
+ return '';
139
+ }
140
+ -function applyStyle(view, value) {
141
+ - if (value == null || value === unsetValue)
142
+ - return;
143
+ +/**
144
+ + * The `style` prop replaces wholesale — a key dropped or nulled since the last
145
+ + * apply resets what it installed (`unsetValue` clears a local CssProperty
146
+ + * value so the cascade can re-apply). String styles are parsed for their
147
+ + * declared names so a later object or removal can reset them.
148
+ + */
149
+ +function applyStyle(node, view, value) {
150
+ + const applied = node.styleKeys;
151
+ + node.styleKeys = undefined;
152
+ if (typeof value === 'string') {
153
+ + clearStyleKeys(view, applied);
154
+ view.setInlineStyle(value);
155
+ + node.styleKeys = inlineStyleKeys(value);
156
+ + return;
157
+ + }
158
+ + if (value == null || value === unsetValue || typeof value !== 'object') {
159
+ + clearStyleKeys(view, applied);
160
+ + return;
161
+ + }
162
+ + if (applied !== undefined) {
163
+ + for (const key of applied) {
164
+ + if (!(key in value))
165
+ + unsetStyleKey(view, key);
166
+ + }
167
+ + }
168
+ + // Mason layout keys target the leaf style under a mason parent —
169
+ + // `view.style` is the NS surface where they land as dead fields. The leaf
170
+ + // style exists only once the parent attaches the view, so entries are
171
+ + // stashed and replayed by applyMasonChildLayout.
172
+ + const parentIsMason = node.parent !== null && MASON_CONTAINER_TAGS.has(node.parent.type);
173
+ + const masonLeaf = parentIsMason ? masonChildStyle(view) : undefined;
174
+ + for (const [key, nextValue] of Object.entries(value)) {
175
+ + if (MASON_LEAF_STYLE_KEYS.has(key)) {
176
+ + const leafStash = view[MASON_LEAF_STYLE_STASH] ?? (view[MASON_LEAF_STYLE_STASH] = {});
177
+ + leafStash[key] = nextValue == null ? undefined : nextValue;
178
+ + if (masonLeaf !== undefined)
179
+ + masonLeaf[key] = nextValue == null ? undefined : nextValue;
180
+ + }
181
+ + if (nextValue == null)
182
+ + unsetStyleKey(view, key);
183
+ + else
184
+ + view.style[key] = nextValue;
185
+ + }
186
+ + node.styleKeys = new Set(Object.keys(value));
187
+ +}
188
+ +function clearStyleKeys(view, keys) {
189
+ + if (keys === undefined)
190
+ + return;
191
+ + for (const key of keys)
192
+ + unsetStyleKey(view, key);
193
+ +}
194
+ +function unsetStyleKey(view, name) {
195
+ + const style = view.style;
196
+ + if (name.startsWith('--')) {
197
+ + style.setUnscopedCssVariable?.(name, unsetValue);
198
+ + return;
199
+ + }
200
+ + style[name] = unsetValue;
201
+ + // A style field outside the CssProperty registry stores the sentinel raw.
202
+ + if (style[name] === unsetValue)
203
+ + style[name] = undefined;
204
+ +}
205
+ +function inlineStyleKeys(style) {
206
+ + const keys = new Set();
207
+ + for (const declaration of style.split(';')) {
208
+ + const colon = declaration.indexOf(':');
209
+ + if (colon === -1)
210
+ + continue;
211
+ + const name = declaration.slice(0, colon).trim();
212
+ + if (name !== '')
213
+ + keys.add(name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()));
214
+ + }
215
+ + return keys;
216
+ +}
217
+ +
218
+ +// --- Mason layout cutover ---------------------------------------------------
219
+ +// masonview is the single layout intrinsic — all layout is expressed in CSS
220
+ +// props (display/flex/grid/position), forwarded onto the node's mason Style.
221
+ +// scrollview stays the platform scroll shell (MasonKit Scroll). Dispatch is
222
+ +// by node tag, so nothing below re-derives the view's class.
223
+ +const MASON_CONTAINER_TAGS = new Set(['masonview', 'scrollview']);
224
+ +// Props whose CssProperty is registered on core's layout classes, never on
225
+ +// MasonKit's ViewBase — `view.flexDirection` would store a dead field, so the
226
+ +// write is forwarded to the mason Style surface instead.
227
+ +// Every former layout-prop name — contract props, LayoutChildProps sugar,
228
+ +// and raw CSS keys alike — is style-only now. As direct props they land on a
229
+ +// dead native field, so the driver warns once per key instead.
230
+ +const MASON_STYLE_ONLY_PROPS = new Set([
231
+ + 'display', 'position', 'inset', 'aspectRatio', 'overflow', 'overflowX',
232
+ + 'overflowY', 'flexBasis', 'justifySelf', 'justifyItems', 'direction',
233
+ + 'boxSizing', 'textAlign', 'scrollbarWidth', 'gridTemplateRows',
234
+ + 'gridTemplateColumns', 'gridTemplateAreas', 'gridAutoFlow', 'gridAutoRows',
235
+ + 'gridAutoColumns', 'gridRow', 'gridColumn', 'gridRowStart', 'gridRowEnd',
236
+ + 'gridColumnStart', 'gridColumnEnd', 'gridArea', 'flexDirection', 'flexWrap',
237
+ + 'justifyContent', 'alignItems', 'alignContent', 'gap', 'rowGap',
238
+ + 'columnGap', 'flexGrow', 'flexShrink', 'alignSelf', 'order', 'row', 'col',
239
+ + 'rowSpan', 'colSpan', 'dock', 'horizontalAlignment', 'verticalAlignment',
240
+ + 'flexWrapBefore', 'stretchLastChild', 'itemWidth', 'itemHeight',
241
+ + 'rows', 'columns',
242
+ +]);
243
+ +// Style-bag keys that mason owns — under a mason parent these are written to
244
+ +// the child's mason leaf style in addition to the NS style surface.
245
+ +const MASON_LEAF_STYLE_KEYS = new Set([
246
+ + ...MASON_STYLE_ONLY_PROPS, 'left', 'top', 'right', 'bottom',
247
+ +]);
248
+ +const MASON_LEAF_STYLE_STASH = Symbol.for('octane-xplat.mason-leaf-style');
249
+ +const warnedStyleOnlyProps = new Set();
250
+ +function warnStyleOnlyProp(node, name) {
251
+ + const key = node.type + ':' + name;
252
+ + if (warnedStyleOnlyProps.has(key))
253
+ + return;
254
+ + warnedStyleOnlyProps.add(key);
255
+ + console.warn(`[xplat] <${node.type} ${name}=...> is a layout key — move it into style={{ ${name}: ... }} or a class.`);
256
+ +}
257
+ +
258
+ +/** A mason Style bound to the node's mason leaf — `_styleHelper` on MasonKit's
259
+ + * own views; for foreign ({N}-native) children, `Style.fromView` binds the
260
+ + * leaf node `nodeForView` creates at insert. Returns undefined until the
261
+ + * child's native view exists (props are stashed and replayed at attach). */
262
+ +function masonChildStyle(view) {
263
+ + if (view === null || view === undefined)
264
+ + return undefined;
265
+ + const helper = view._styleHelper;
266
+ + if (helper !== undefined)
267
+ + return helper;
268
+ + const native = view.nativeViewProtected;
269
+ + if (!native)
270
+ + return undefined;
271
+ + try {
272
+ + return MasonStyle.fromView(view, native);
273
+ + }
274
+ + catch {
275
+ + return undefined;
276
+ + }
277
+ +}
278
+ +
279
+ +/** NS attached alignment -> CSS self-alignment keyword. */
280
+ +function masonAlignKeyword(value) {
281
+ + switch (value) {
282
+ + case 'left':
283
+ + case 'top':
284
+ + return 'start';
285
+ + case 'right':
286
+ + case 'bottom':
287
+ + return 'end';
288
+ + case 'center':
289
+ + case 'middle':
290
+ + return 'center';
291
+ + case 'stretch':
292
+ + return 'stretch';
293
+ + }
294
+ + return undefined;
295
+ +}
296
+ +
297
+ +/** Container-role defaults — equivalent to each NS layout's built-in mode. */
298
+ +function applyMasonDefaults(type, view) {
299
+ + const style = view.style;
300
+ + if (type === 'scrollview') {
301
+ + style.display = 'flex';
302
+ + style.flexDirection = 'column';
303
+ + style.overflowY = 'scroll';
304
+ + style.overflowX = 'hidden';
305
+ + }
306
+ + style.alignItems = 'stretch';
307
+ + // Containers establish the containing block for absolute descendants.
308
+ + style.position = 'relative';
309
+ +}
310
+ +
311
+ +/** Container-role prop -> mason style write. Runs for every prop on a mason
312
+ + * container; only the scrollview chrome keys (orientation/scroll toggles)
313
+ + * still translate — everything else is style-only. */
314
+ +function applyMasonContainerProp(node, view, name, value) {
315
+ + const style = view.style;
316
+ + const unset = value == null || value === unsetValue;
317
+ + if (node.type === 'scrollview') {
318
+ + if (name === 'orientation' || name === 'horizontal') {
319
+ + const horizontal = !unset && (value === 'horizontal' || value === true);
320
+ + style.flexDirection = horizontal ? 'row' : 'column';
321
+ + style.overflowX = horizontal ? 'scroll' : 'hidden';
322
+ + style.overflowY = horizontal ? 'hidden' : 'scroll';
323
+ + }
324
+ + if (name === 'isScrollEnabled') {
325
+ + const enabled = !unset && value !== false;
326
+ + style.overflowX = enabled ? 'scroll' : 'hidden';
327
+ + style.overflowY = enabled ? 'scroll' : 'hidden';
328
+ + }
329
+ + }
330
+ +}
331
+ +
332
+ +/** Replay a child's authored mason style entries onto its leaf — the leaf
333
+ + * style only exists once the parent attaches the view, so `applyStyle`
334
+ + * stashes leaf-intended keys and every attach replays them. Sibling order is
335
+ + * irrelevant to the replay, but the pass runs per sibling anyway so a child
336
+ + * inserted later still gets its stash flushed. */
337
+ +function applyMasonChildLayout(node, parent) {
338
+ + const view = node.view;
339
+ + if (view === null || view === undefined)
340
+ return;
341
+ + const style = masonChildStyle(view);
342
+ + if (style === undefined)
343
+ + return;
344
+ + const leafStash = view[MASON_LEAF_STYLE_STASH];
345
+ + if (leafStash !== undefined) {
346
+ + for (const key of Object.keys(leafStash))
347
+ + style[key] = leafStash[key];
348
+ }
349
+ - if (typeof value === 'object')
350
+ - Object.assign(view.style, value);
351
+ }
352
+ +
353
+ /** Recompute a text host's `text` from its `#text` children. */
354
+ function syncText(parent) {
355
+ if (parent === null || isPortalTarget(parent))
356
+ @@ -190,6 +463,15 @@ function addViewChild(parentView, view, index) {
357
+ parentView.insertChild(view, Math.min(index, parentView.getChildrenCount()));
358
+ return;
359
+ }
360
+ + // Custom-layout containers that own their child list without subclassing
361
+ + // LayoutBase (e.g. MasonKit ViewBase — CustomLayoutView → ContainerView).
362
+ + // Plain View/ContainerView lacks insertChild, so this stays a capability
363
+ + // check rather than a catch-all.
364
+ + if (typeof parentView.insertChild === 'function' && typeof parentView.removeChild === 'function') {
365
+ + const count = typeof parentView.getChildrenCount === 'function' ? parentView.getChildrenCount() : index;
366
+ + parentView.insertChild(view, Math.min(index, count));
367
+ + return;
368
+ + }
369
+ if (parentView instanceof ContentView) {
370
+ parentView.content = view;
371
+ return;
372
+ @@ -234,6 +516,13 @@ function removeViewChild(parentView, view) {
373
+ parentView.removeChild(view);
374
+ return;
375
+ }
376
+ + // Same capability check as addViewChild: custom-layout containers that
377
+ + // aren't LayoutBase subclasses remove children through removeChild.
378
+ + if (typeof parentView.removeChild === 'function' && typeof parentView.insertChild === 'function') {
379
+ + if (view.parent === parentView)
380
+ + parentView.removeChild(view);
381
+ + return;
382
+ + }
383
+ if (parentView instanceof ContentView) {
384
+ if (parentView.content === view)
385
+ parentView.content = CLEARED;
386
+ @@ -370,6 +659,13 @@ function attach(container, node) {
387
+ }
388
+ }
389
+ addViewChild(parentView, node.view, index);
390
+ + // The child's mason leaf node exists only once the parent has attached it;
391
+ + // replay placement for every sibling (dock's stretchLastChild depends on
392
+ + // the full sibling list).
393
+ + if (parent !== null && MASON_CONTAINER_TAGS.has(parent.type)) {
394
+ + for (const sibling of childrenOf(container, parent))
395
+ + applyMasonChildLayout(sibling, parent);
396
+ + }
397
+ markTabView(tabViewOwning(node));
398
+ }
399
+ function insert(container, parent, node, before) {
400
+ @@ -548,7 +844,7 @@ function preparePortalTarget(context) {
401
+ }
402
+ const target = owner?.node.view ?? suppliedTarget;
403
+ if (!(target instanceof LayoutBase)) {
404
+ - throw new TypeError(`NativeScript driver: a <${target.typeName}> view cannot host portal children; target a layout (e.g. rootlayout, gridlayout).`);
405
+ + throw new TypeError(`NativeScript driver: a <${target.typeName}> view cannot host portal children; target a layout (e.g. rootlayout, masonview).`);
406
+ }
407
+ let id = portalTargetIds.get(target);
408
+ if (id === undefined) {
409
+ @@ -641,7 +937,7 @@ function applyCommand(container, command) {
410
+ }
411
+ export const nativeScriptDriver = {
412
+ id: NATIVESCRIPT_RENDERER_ID,
413
+ - capabilities: { text: 'host' },
414
+ + capabilities: { text: 'host', visibility: true },
415
+ events: {
416
+ classify(name) {
417
+ if (!EVENT_PROP.test(name))
418
+ diff --git a/dist/elements.js b/dist/elements.js
419
+ index a13bcea..e9988e1 100644
420
+ --- a/dist/elements.js
421
+ +++ b/dist/elements.js
422
+ @@ -1,4 +1,7 @@
423
+ -import { AbsoluteLayout, ActionBar, ActionItem, ActivityIndicator, Button, LiquidGlass, ContentView, DatePicker, DockLayout, FlexboxLayout, FormattedString, Frame, GridLayout, HtmlView, Image, Label, ListPicker, ListView, NavigationButton, Page, Placeholder, Progress, ProxyViewContainer, RootLayout, ScrollView, SearchBar, SegmentedBar, SegmentedBarItem, Slider, Span, StackLayout, Switch, TabView, TabViewItem, TextField, TextView, TimePicker, WebView, WrapLayout, } from '@nativescript/core';
424
+ +import { ActionBar, ActionItem, ActivityIndicator, Button, LiquidGlass, ContentView, DatePicker, FormattedString, Frame, HtmlView, Image, Label, ListPicker, ListView, NavigationButton, Page, Placeholder, Progress, ProxyViewContainer, RootLayout, SearchBar, SegmentedBar, SegmentedBarItem, Slider, Span, Switch, TabView, TabViewItem, TextField, TextView, TimePicker, WebView, } from '@nativescript/core';
425
+ +// Mason cutover: container intrinsics resolve to MasonKit views; the driver
426
+ +// translates their NativeScript-shaped props into mason style writes.
427
+ +import { View as MasonView, Scroll as MasonScroll } from '@triniwiz/nativescript-masonkit';
428
+ /**
429
+ * Lowercase tag name -> view constructor. The keys are the JSX tag names of
430
+ * `intrinsics.ts`; an app extends the vocabulary with `registerElement`.
431
+ @@ -28,18 +31,14 @@ export function onElementReplaced(listener) {
432
+ };
433
+ }
434
+ const BUILTIN_ELEMENTS = [
435
+ - ['absolutelayout', AbsoluteLayout],
436
+ ['actionbar', ActionBar],
437
+ ['actionitem', ActionItem],
438
+ ['activityindicator', ActivityIndicator],
439
+ ['button', Button],
440
+ ['contentview', ContentView],
441
+ ['datepicker', DatePicker],
442
+ - ['docklayout', DockLayout],
443
+ - ['flexboxlayout', FlexboxLayout],
444
+ ['formattedstring', FormattedString],
445
+ ['frame', Frame],
446
+ - ['gridlayout', GridLayout],
447
+ ['htmlview', HtmlView],
448
+ ['image', Image],
449
+ ['label', Label],
450
+ @@ -51,13 +50,12 @@ const BUILTIN_ELEMENTS = [
451
+ ['progress', Progress],
452
+ ['proxyviewcontainer', ProxyViewContainer],
453
+ ['rootlayout', RootLayout],
454
+ - ['scrollview', ScrollView],
455
+ + ['scrollview', MasonScroll],
456
+ ['searchbar', SearchBar],
457
+ ['segmentedbar', SegmentedBar],
458
+ ['segmentedbaritem', SegmentedBarItem],
459
+ ['slider', Slider],
460
+ ['span', Span],
461
+ - ['stacklayout', StackLayout],
462
+ ['switch', Switch],
463
+ ['tabview', TabView],
464
+ ['tabviewitem', TabViewItem],
465
+ @@ -65,8 +63,8 @@ const BUILTIN_ELEMENTS = [
466
+ ['textview', TextView],
467
+ ['timepicker', TimePicker],
468
+ ['webview', WebView],
469
+ - ['wraplayout', WrapLayout],
470
+ ['liquidglass', LiquidGlass],
471
+ + ['masonview', MasonView],
472
+ ];
473
+ for (const [tag, element] of BUILTIN_ELEMENTS)
474
+ registerElement(tag, element);
475
+ diff --git a/dist/intrinsics.d.ts b/dist/intrinsics.d.ts
476
+ index 45a47cb..668c920 100644
477
+ --- a/dist/intrinsics.d.ts
478
+ +++ b/dist/intrinsics.d.ts
479
+ @@ -24,9 +24,9 @@
480
+ */
481
+ import type * as NS from '@nativescript/core';
482
+ export type NativeScriptNode = unknown;
483
+ -type Ref<TInstance> = ((instance: TInstance | null) => void) | {
484
+ +type Ref<TInstance> = ((instance: TInstance | null) => void | (() => void)) | {
485
+ current: TInstance | null;
486
+ -} | null;
487
+ +} | readonly Ref<TInstance>[] | null | undefined;
488
+ /**
489
+ * The driver assigns every non-event prop onto the view instance, so a JSX
490
+ * attribute is a NativeScript view property rather than a DOM attribute.
491
+ @@ -76,34 +76,67 @@ export interface CommonEvents<TInstance> {
492
+ export type ClassValue = string | boolean | null | undefined | readonly ClassValue[] | {
493
+ readonly [className: string]: unknown;
494
+ };
495
+ +/** Mason engine style keys accepted through `style`/`className` — under a
496
+ + * masonview parent a child's leaf style consumes them; on a masonview they
497
+ + * are the container's own layout. These are the only way to set raw layout
498
+ + * properties: direct props of the same names are rejected by the driver. */
499
+ +export interface MasonStyleProps {
500
+ + display?: string;
501
+ + position?: string;
502
+ + inset?: number | string;
503
+ + left?: number | string;
504
+ + top?: number | string;
505
+ + right?: number | string;
506
+ + bottom?: number | string;
507
+ + direction?: string;
508
+ + boxSizing?: string;
509
+ + flexDirection?: string;
510
+ + flexWrap?: string;
511
+ + justifyContent?: string;
512
+ + justifyItems?: string;
513
+ + justifySelf?: string;
514
+ + alignItems?: string;
515
+ + alignContent?: string;
516
+ + alignSelf?: string;
517
+ + textAlign?: string;
518
+ + gap?: number | string;
519
+ + rowGap?: number | string;
520
+ + columnGap?: number | string;
521
+ + flexGrow?: number;
522
+ + flexShrink?: number;
523
+ + flexBasis?: number | string;
524
+ + order?: number;
525
+ + aspectRatio?: number;
526
+ + overflow?: string;
527
+ + overflowX?: string;
528
+ + overflowY?: string;
529
+ + scrollbarWidth?: number;
530
+ + gridTemplateRows?: string;
531
+ + gridTemplateColumns?: string;
532
+ + gridTemplateAreas?: string;
533
+ + gridAutoFlow?: string;
534
+ + gridAutoRows?: string;
535
+ + gridAutoColumns?: string;
536
+ + gridRow?: string;
537
+ + gridColumn?: string;
538
+ + gridRowStart?: string | number;
539
+ + gridRowEnd?: string | number;
540
+ + gridColumnStart?: string | number;
541
+ + gridColumnEnd?: string | number;
542
+ + gridArea?: string;
543
+ +}
544
+ export interface CommonAttributes {
545
+ /** Resolved against `app.css`. */
546
+ className?: ClassValue;
547
+ class?: ClassValue;
548
+ /** A string is parsed as inline CSS; an object is assigned onto `view.style`. */
549
+ - style?: string | Partial<NS.Style>;
550
+ + style?: string | Partial<NS.Style> | MasonStyleProps;
551
+ /**
552
+ * Name of the property on a slot-hosting parent (e.g. a drawer's
553
+ * `mainContent` / `leftDrawer`) this child should be assigned to instead of
554
+ * being added as a regular layout child.
555
+ */
556
+ hostSlot?: string;
557
+ - /**
558
+ - * Attached layout properties. The layout modules register these on `View` at
559
+ - * runtime, so they are absent from the `@nativescript/core` class typings.
560
+ - */
561
+ - row?: number | string;
562
+ - col?: number | string;
563
+ - rowSpan?: number | string;
564
+ - colSpan?: number | string;
565
+ - dock?: 'top' | 'right' | 'bottom' | 'left';
566
+ - left?: number | string;
567
+ - top?: number | string;
568
+ - flexGrow?: number | string;
569
+ - flexShrink?: number | string;
570
+ - flexWrapBefore?: boolean | string;
571
+ - alignSelf?: 'auto' | 'flex-start' | 'flex-end' | 'center' | 'baseline' | 'stretch';
572
+ - order?: number | string;
573
+ }
574
+ export interface ListViewAttributes {
575
+ /**
576
+ @@ -117,25 +150,21 @@ export interface ListViewAttributes {
577
+ }
578
+ export interface OctaneAttributes<TInstance> {
579
+ key?: string | number;
580
+ - ref?: Ref<TInstance> | readonly Ref<TInstance>[];
581
+ + ref?: Ref<TInstance>;
582
+ children?: NativeScriptNode;
583
+ }
584
+ type Overridden = keyof CommonAttributes | keyof OctaneAttributes<unknown> | keyof CommonEvents<unknown>;
585
+ /** Props of a JSX tag backed by a `@nativescript/core`-style view class; the type to use for app-registered tags. */
586
+ export type Attributes<TClass extends abstract new (...args: never[]) => unknown> = Omit<ViewProperties<InstanceType<TClass>> & DerivedEvents<TClass, InstanceType<TClass>>, Overridden> & CommonAttributes & CommonEvents<InstanceType<TClass>> & OctaneAttributes<InstanceType<TClass>>;
587
+ export interface NativeScriptElements {
588
+ - absolutelayout: Attributes<typeof NS.AbsoluteLayout>;
589
+ actionbar: Attributes<typeof NS.ActionBar>;
590
+ actionitem: Attributes<typeof NS.ActionItem>;
591
+ activityindicator: Attributes<typeof NS.ActivityIndicator>;
592
+ button: Attributes<typeof NS.Button>;
593
+ contentview: Attributes<typeof NS.ContentView>;
594
+ datepicker: Attributes<typeof NS.DatePicker>;
595
+ - docklayout: Attributes<typeof NS.DockLayout>;
596
+ - flexboxlayout: Attributes<typeof NS.FlexboxLayout>;
597
+ formattedstring: Attributes<typeof NS.FormattedString>;
598
+ frame: Attributes<typeof NS.Frame>;
599
+ - gridlayout: Attributes<typeof NS.GridLayout>;
600
+ htmlview: Attributes<typeof NS.HtmlView>;
601
+ image: Attributes<typeof NS.Image>;
602
+ label: Attributes<typeof NS.Label>;
603
+ @@ -153,7 +182,6 @@ export interface NativeScriptElements {
604
+ segmentedbaritem: Attributes<typeof NS.SegmentedBarItem>;
605
+ slider: Attributes<typeof NS.Slider>;
606
+ span: Attributes<typeof NS.Span>;
607
+ - stacklayout: Attributes<typeof NS.StackLayout>;
608
+ switch: Attributes<typeof NS.Switch>;
609
+ tabview: Attributes<typeof NS.TabView>;
610
+ tabviewitem: Attributes<typeof NS.TabViewItem>;
611
+ @@ -161,7 +189,8 @@ export interface NativeScriptElements {
612
+ textview: Attributes<typeof NS.TextView>;
613
+ timepicker: Attributes<typeof NS.TimePicker>;
614
+ webview: Attributes<typeof NS.WebView>;
615
+ - wraplayout: Attributes<typeof NS.WrapLayout>;
616
+ + /** The single layout container — all layout flows through `style`/`className`; layout prop names are rejected by the driver. */
617
+ + masonview: Attributes<typeof NS.View>;
618
+ /** iOS 26 glass surface (plain layout elsewhere); behaves as a GridLayout. */
619
+ liquidglass: Attributes<typeof NS.LiquidGlass>;
620
+ }
@@ -0,0 +1,13 @@
1
+ diff --git a/dist/android-tools-info.js b/dist/android-tools-info.js
2
+ index ed84fd1c23bbc3161d40fdd7c65dbd842704696c..ef647fb6b834a5fdf4d09da5e920e8befce1e6f2 100644
3
+ --- a/dist/android-tools-info.js
4
+ +++ b/dist/android-tools-info.js
5
+ @@ -383,7 +383,7 @@ class AndroidToolsInfo {
6
+ });
7
+ if (foundRuntime) {
8
+ let version = content.devDependencies[foundRuntime];
9
+ - if (version.includes("tgz")) {
10
+ + if (version.includes("tgz") || version.startsWith("catalog:")) {
11
+ try {
12
+ const packagePath = require.resolve(`${foundRuntime}/package.json`, {
13
+ paths: [projectDir],
@@ -107,6 +107,43 @@ index a05696b679b3feecd1bafa267de44b59db559162..06648067a3fcf7f8c74437a82b94ec01
107
107
  }
108
108
  return ['.native', ''];
109
109
  }
110
+ diff --git a/hmr/helpers/ast-normalizer.js b/hmr/helpers/ast-normalizer.js
111
+ index c6113c74247db616609b87d7f365c4d4c199c934..73651a1fd1892555cc5dd57bcefb9e93d30516b6 100644
112
+ --- a/hmr/helpers/ast-normalizer.js
113
+ +++ b/hmr/helpers/ast-normalizer.js
114
+ @@ -86,9 +86,10 @@ export function astNormalizeModuleImportsAndHelpers(code, opts) {
115
+ // path (e.g. `html-entities` when not in optimizeDeps) — those must be
116
+ // rewritten to the bare id (see below), not removed; removing them
117
+ // left consumer bindings undefined (`decode is not defined`).
118
+ + // User virtual stylesheets must reach the native CSS registration route.
119
+ // Exemption: oxc runtime helper virtuals are real servable URLs that
120
+ // downstream passes preserve/origin-prefix — keep them here too.
121
+ - const isViteVirtual = (src) => !!src && (/\0/.test(src) || /__x00__/.test(src)) && !src.includes('__x00__@oxc-project+runtime');
122
+ + const isViteVirtual = (src) => !!src && (/\0/.test(src) || /__x00__/.test(src)) && !src.includes('__x00__@oxc-project+runtime') && !/\.(css|scss|sass|less|styl|stylus)(?:[?#]|$)/i.test(src);
123
+ const realIdPrefixed = (src) => {
124
+ if (!src || !src.startsWith('/@id/'))
125
+ return null;
126
+ diff --git a/hmr/server/css-device-module.js b/hmr/server/css-device-module.js
127
+ index 0d7cd968eac690a85411cf3d6a24323a3d3a3992..98094d6ccfb918d00249d8087aebdd994b64244d 100644
128
+ --- a/hmr/server/css-device-module.js
129
+ +++ b/hmr/server/css-device-module.js
130
+ @@ -41,4 +41,15 @@ export function buildCssRegisterSnippet(tag, cssText) {
131
+ export function buildCssRegisterSnippetFromVar(tag, varName) {
132
+ return buildRegisterCall(JSON.stringify(tag), varName);
133
+ }
134
+ +/** Bridge only plugin-provided virtual stylesheets; file CSS has its own hot-update path. */
135
+ +export function virtualCssUpdates(payload) {
136
+ + if (payload?.type !== 'update') return [];
137
+ + return (payload.updates || []).flatMap((update) => {
138
+ + if (update.type !== 'css-update' && update.type !== 'js-update') return [];
139
+ + let spec = update.path?.split(/[?#]/)[0];
140
+ + if (spec?.startsWith('\0')) spec = '/@id/' + spec.replace(/\0/g, '__x00__');
141
+ + if (!spec?.startsWith('/@id/__x00__') || !CSS_MODULE_RE.test(spec)) return [];
142
+ + return [{ type: 'css-update', path: '/ns/m' + spec, acceptedPath: '/ns/m' + spec, tag: spec, timestamp: update.timestamp }];
143
+ + });
144
+ +}
145
+ //# sourceMappingURL=css-device-module.js.map
146
+
110
147
  diff --git a/hmr/server/deps-bundle.js b/hmr/server/deps-bundle.js
111
148
  index 8f2fa15556244f7ef811b5e2ea0fc6a19bab74d3..80137262ec46d347d39a7ee12d66d649b5f909f5 100644
112
149
  --- a/hmr/server/deps-bundle.js
@@ -166,11 +203,41 @@ index 71574c48f655183af0c9601b020fd1febd63ccb3..0061d9c9e9fe9c6034576eae8449831d
166
203
  if (!imports.length) {
167
204
  return code;
168
205
  }
206
+ diff --git a/hmr/server/process-code-for-device.js b/hmr/server/process-code-for-device.js
207
+ index c71d457806a8add919f7e2368e7ebc349bd908e7..998eb268ed0db1200f8b5ce30fc7da211125406f 100644
208
+ --- a/hmr/server/process-code-for-device.js
209
+ +++ b/hmr/server/process-code-for-device.js
210
+ @@ -1,3 +1,4 @@
211
+ +import { CSS_MODULE_RE } from './css-device-module.js';
212
+ // Device code-transform: cleans Vite/framework noise out of a served module,
213
+ // injects NativeScript build globals + the process.env shim, normalizes
214
+ // imports/helpers for the on-device ESM runtime, and collects an importer's
215
+ @@ -451,13 +452,13 @@ function processCodeForDevice(code, isVitePreBundled, preserveVendorImports = fa
216
+ // Unlike other virtual ids these are real, servable dev-server URLs returning
217
+ // self-contained ESM — stripping them leaves `_decorate`/`_decorateMetadata`
218
+ // undefined at runtime. They're origin-prefixed for the device in rewriteImports.
219
+ - result = result.replace(/import\s+[^;]+\s+from\s+["']\/@id\/(?!__x00__@oxc-project\+runtime)[^"']*["'];?\s*/g, '');
220
+ + result = result.replace(/import\s+[^;]+\s+from\s+["'](\/@id\/(?!__x00__@oxc-project\+runtime)[^"']*)["'];?\s*/g, (statement, spec) => CSS_MODULE_RE.test(spec) ? statement : '');
221
+ // Also remove side-effect only virtual id imports like: import "/@id/__x00__vite/dynamic-import-helper.js";
222
+ // These can slip through and cause the NativeScript runtime to attempt resolving
223
+ // a non-existent physical file (e.g. /@id/__x00__vite/dynamic-import-helper.js) leading to
224
+ // module not found crashes during HMR evaluation.
225
+ if (/^[\t ]*import\s+["']\/@id\/[^"']+["'];?\s*$/m.test(result)) {
226
+ - result = result.replace(/^[\t ]*import\s+["']\/@id\/[^"']+["'];?\s*$/gm, '');
227
+ + result = result.replace(/^[\t ]*import\s+["'](\/@id\/[^"']+)["'];?\s*$/gm, (statement, spec) => CSS_MODULE_RE.test(spec) ? statement : '');
228
+ // Inject a lightweight marker comment (harmless if left in output) so devs can confirm rewrite took place when viewing streamed artifact.
229
+ result = `// [hmr-sanitize] stripped virtual /@id/ side-effect imports\n${result}`;
230
+ }
169
231
  diff --git a/hmr/server/rewrite-imports.js b/hmr/server/rewrite-imports.js
170
- index 4a6575cb00a6aeecbbb7ac5ccac3268cb10d7f54..ea97dfe6c895bb6b80b0c0403c86663046c65496 100644
232
+ index 4a6575cb00a6aeecbbb7ac5ccac3268cb10d7f54..2876cf3694c278249b9db182e2f89df34cc464f3 100644
171
233
  --- a/hmr/server/rewrite-imports.js
172
234
  +++ b/hmr/server/rewrite-imports.js
173
- @@ -12,7 +12,93 @@ import { resolveAngularCoreHmrImportSource, rewriteAngularEntryRegisterOnly } fr
235
+ @@ -1,3 +1,4 @@
236
+ +import { CSS_MODULE_RE } from './css-device-module.js';
237
+ // THE SINGLE import-rewrite function — used everywhere for consistency. Maps a
238
+ // served module's specifiers to device-reachable forms: the unified /ns/core
239
+ // bridge, /ns/m node_modules + app modules, /ns/sfc for Vue, vendor bare specs,
240
+ @@ -12,7 +13,93 @@ import { resolveAngularCoreHmrImportSource, rewriteAngularEntryRegisterOnly } fr
174
241
  import { isNativeScriptCoreModule, isNativeScriptPluginModule, normalizeNativeScriptCoreSpecifier, normalizeNodeModulesSpecifier, resolveNodeModulesPackageBoundary, resolveVendorRouting, rewriteFsAbsoluteToNsM, shouldPreserveBareRuntimePluginSubpathImport } from './websocket-module-specifiers.js';
175
242
  import { ensureDynamicHmrImportHelper } from './websocket-served-module-helpers.js';
176
243
  import { collectMixedRuntimePluginHttpRootPackages, findDependencyFileName, getProjectRelativeImportPath, isApplicationImport, normalizeImportPath, stripToProjectRelative, toAppModuleBaseId, toNodeModulesHttpModuleId } from './device-transform-helpers.js';
@@ -264,7 +331,7 @@ index 4a6575cb00a6aeecbbb7ac5ccac3268cb10d7f54..ea97dfe6c895bb6b80b0c0403c866630
264
331
  export function prepareAngularEntryForDevice(code, importerPath, sfcFileMap, depFileMap, projectRoot, verbose = false, outputDirOverrideRel, httpOrigin, resolveVendorAsHttp = false) {
265
332
  const rewrittenCode = rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot, verbose, outputDirOverrideRel, httpOrigin, resolveVendorAsHttp);
266
333
  return rewriteAngularEntryRegisterOnly(rewrittenCode, resolveAngularCoreHmrImportSource(rewrittenCode, httpOrigin));
267
- @@ -236,6 +322,19 @@ export function rewriteImports(code, importerPath, sfcFileMap, depFileMap, proje
334
+ @@ -236,6 +323,19 @@ export function rewriteImports(code, importerPath, sfcFileMap, depFileMap, proje
268
335
  }
269
336
  return `${prefix}${stub}${suffix}`;
270
337
  }
@@ -284,7 +351,7 @@ index 4a6575cb00a6aeecbbb7ac5ccac3268cb10d7f54..ea97dfe6c895bb6b80b0c0403c866630
284
351
  spec = normalizeNativeScriptCoreSpecifier(spec);
285
352
  // Pull `/@fs/<abs-path>` URLs back into the `/ns/m/` pipeline so they
286
353
  // hit our CJS/UMD-wrapping handler. Vite emits `/@fs/...` for any
287
- @@ -253,11 +352,29 @@ export function rewriteImports(code, importerPath, sfcFileMap, depFileMap, proje
354
+ @@ -253,17 +353,39 @@ export function rewriteImports(code, importerPath, sfcFileMap, depFileMap, proje
288
355
  }
289
356
  return `${prefix}${rewritten}${suffix}`;
290
357
  }
@@ -319,17 +386,32 @@ index 4a6575cb00a6aeecbbb7ac5ccac3268cb10d7f54..ea97dfe6c895bb6b80b0c0403c866630
319
386
  }
320
387
  // Vite 8 oxc runtime helpers (decorators etc.) — emitted as
321
388
  // `/@id/__x00__@oxc-project+runtime@<ver>/helpers/<name>.js`. These are
389
+ // real dev-server URLs serving self-contained ESM, so just origin-prefix
390
+ // them for the device; no /ns/m wrapping needed (processCodeForDevice
391
+ // exempts them from the generic /@id/ strip for the same reason).
392
+ + // User virtual CSS needs native compilation/registration, never the DOM module.
393
+ + if (spec.startsWith('/@id/') && CSS_MODULE_RE.test(spec)) {
394
+ + return `${prefix}${httpOriginSafe || ''}/ns/m${spec}${suffix}`;
395
+ + }
396
+ if (spec.startsWith('/@id/__x00__@oxc-project+runtime')) {
397
+ if (httpOriginSafe) {
398
+ return `${prefix}${httpOriginSafe}${spec}${suffix}`;
322
399
  diff --git a/hmr/server/websocket-module-specifiers.js b/hmr/server/websocket-module-specifiers.js
323
- index 74f50a77f48f3873aebf375a29d06b16c29440a3..cc196fb6b92d48179bf8e124ba2a2aa9b13796f9 100644
400
+ index 74f50a77f48f3873aebf375a29d06b16c29440a3..0f06b6dc997118473e4a1ac6d3a3c48432311c71 100644
324
401
  --- a/hmr/server/websocket-module-specifiers.js
325
402
  +++ b/hmr/server/websocket-module-specifiers.js
326
- @@ -1,4 +1,4 @@
403
+ @@ -1,8 +1,8 @@
327
404
  -import { existsSync, readdirSync, readFileSync, realpathSync } from 'fs';
328
405
  +import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'fs';
329
406
  import * as path from 'path';
330
407
  import * as PAT from './constants.js';
331
408
  import { getVendorManifest, resolveVendorSpecifier } from '../shared/vendor/registry.js';
332
- @@ -328,6 +328,60 @@ export function resolveVendorFromCandidate(specifier) {
409
+ -import { getProjectRootPath } from '../../helpers/project.js';
410
+ +import { getMonorepoWorkspaceRoot, getProjectRootPath } from '../../helpers/project.js';
411
+ import { extractRootPackageName, getPackageRuntimeInfo } from '../shared/package-classifier.js';
412
+ import { getFlavorClientPackages } from '../framework-flavors.js';
413
+ const ESM_FRAMEWORK_PACKAGE_ROOTS = new Set(['@nativescript/angular', 'nativescript-angular']);
414
+ @@ -328,6 +328,76 @@ export function resolveVendorFromCandidate(specifier) {
333
415
  }
334
416
  return null;
335
417
  }
@@ -339,7 +421,8 @@ index 74f50a77f48f3873aebf375a29d06b16c29440a3..cc196fb6b92d48179bf8e124ba2a2aa9
339
421
  + * its workspace layout — consulted when neither the project root nor the
340
422
  + * workspace root can anchor a served `/node_modules/` tail:
341
423
  + * - `link:`/`file:`/`portal:` deps keep their own `node_modules` at the real
342
- + * target; a linked package's transitive deps never reach the app's store.
424
+ + * target; their workspace siblings can own transitive deps too. Reuse the
425
+ + * bounded workspace scan rather than recursively walking dependency trees.
343
426
  + * - pnpm's isolated virtual store keeps every non-hoisted package under
344
427
  + * `node_modules/.pnpm/<name>@<ver>/node_modules/`.
345
428
  + * Importer info is gone from the URL — first hit wins, same as the workspace
@@ -351,9 +434,13 @@ index 74f50a77f48f3873aebf375a29d06b16c29440a3..cc196fb6b92d48179bf8e124ba2a2aa9
351
434
  + return cached;
352
435
  + }
353
436
  + const dirs = [];
437
+ + const seen = new Set();
438
+ + const linkedWorkspaces = new Set();
354
439
  + const pushIfPresent = (dir) => {
355
- + if (existsSync(dir))
440
+ + if (!seen.has(dir) && existsSync(dir)) {
441
+ + seen.add(dir);
356
442
  + dirs.push(dir);
443
+ + }
357
444
  + };
358
445
  + const pnpmStore = path.join(projectRoot, 'node_modules', '.pnpm');
359
446
  + try {
@@ -374,8 +461,12 @@ index 74f50a77f48f3873aebf375a29d06b16c29440a3..cc196fb6b92d48179bf8e124ba2a2aa9
374
461
  + continue;
375
462
  + try {
376
463
  + const depDir = realpathSync(path.resolve(projectRoot, target));
377
- + if (statSync(depDir).isDirectory())
464
+ + if (statSync(depDir).isDirectory()) {
378
465
  + pushIfPresent(path.join(depDir, 'node_modules'));
466
+ + const workspace = getMonorepoWorkspaceRoot(depDir);
467
+ + if (workspace)
468
+ + linkedWorkspaces.add(workspace);
469
+ + }
379
470
  + }
380
471
  + catch {
381
472
  + }
@@ -384,13 +475,20 @@ index 74f50a77f48f3873aebf375a29d06b16c29440a3..cc196fb6b92d48179bf8e124ba2a2aa9
384
475
  + }
385
476
  + catch {
386
477
  + }
478
+ + // Preserve direct-dependency precedence. Scan each linked workspace once,
479
+ + // using the existing two-level package scan and its per-workspace cache.
480
+ + for (const workspace of linkedWorkspaces) {
481
+ + pushIfPresent(path.join(workspace, 'node_modules'));
482
+ + for (const dir of listWorkspaceNodeModulesDirs(workspace))
483
+ + pushIfPresent(dir);
484
+ + }
387
485
  + depFallbackDirsCache.set(projectRoot, dirs);
388
486
  + return dirs;
389
487
  +}
390
488
  const workspaceNodeModulesDirsCache = new Map();
391
489
  /**
392
490
  * `node_modules` dirs that can hold a package the served URL no longer
393
- @@ -428,6 +482,16 @@ export function resolveCandidateFilePath(candidate, projectRoot, workspaceRoot)
491
+ @@ -428,6 +498,16 @@ export function resolveCandidateFilePath(candidate, projectRoot, workspaceRoot)
394
492
  }
395
493
  }
396
494
  }
@@ -407,14 +505,26 @@ index 74f50a77f48f3873aebf375a29d06b16c29440a3..cc196fb6b92d48179bf8e124ba2a2aa9
407
505
  return null;
408
506
  }
409
507
  /**
410
- @@ -557,22 +621,17 @@ export function filterExistingNodeModulesTransformCandidates(spec, candidates, p
508
+ @@ -512,8 +592,8 @@ export function rewriteFsAbsoluteToNsM(spec, projectRoot, workspaceRoot) {
509
+ * `export const X: string = '...'`. Routing through the realpath makes
510
+ * Vite see a non-`node_modules` path and the TS transform applies.
511
+ *
512
+ - * App-local candidates whose real path is also app-local are returned
513
+ - * unchanged so existing behaviour is preserved bit-for-bit.
514
+ + * Candidates are returned unchanged only when their URL addresses the file
515
+ + * found. Fallback files need filesystem anchors even inside the app root.
516
+ */
517
+ export function filterExistingNodeModulesTransformCandidates(spec, candidates, projectRoot, workspaceRoot) {
518
+ const cleanedSpec = spec.replace(PAT.QUERY_PATTERN, '');
519
+ @@ -557,22 +637,18 @@ export function filterExistingNodeModulesTransformCandidates(spec, candidates, p
411
520
  // through to the existing project/workspace logic.
412
521
  }
413
522
  if (mapped === null) {
414
523
  - if (!hasDistinctWorkspaceRoot) {
415
- + const relFromProject = path.relative(resolvedProjectRoot, resolved);
416
- + const insideProjectRoot = relFromProject !== '' && !relFromProject.startsWith('..') && !path.isAbsolute(relFromProject);
417
- + if (insideProjectRoot) {
524
+ + const candidatePath = cleanedCandidate.startsWith('/@fs/')
525
+ + ? path.resolve(cleanedCandidate.slice('/@fs'.length))
526
+ + : path.resolve(resolvedProjectRoot, cleanedCandidate.replace(/^\/+/, ''));
527
+ + if (candidatePath === resolved) {
418
528
  mapped = candidate;
419
529
  }
420
530
  else {
@@ -430,10 +540,10 @@ index 74f50a77f48f3873aebf375a29d06b16c29440a3..cc196fb6b92d48179bf8e124ba2a2aa9
430
540
  - // hoisted package's `exports` field may refuse.
431
541
  - mapped = `/@fs${resolved.replace(/\\/g, '/')}${querySuffix}`;
432
542
  - }
433
- + // File resolves outside the project root — hoisted workspace
434
- + // node_modules, pnpm's `.pnpm` store, or a link:/file:/portal:
435
- + // dep's own node_modules. Serve the fs-anchored URL so Vite
436
- + // reads it directly; the root-relative URL would miss.
543
+ + // The candidate URL does not address the discovered file. A
544
+ + // fallback may live inside the project (nested link: dependency
545
+ + // or pnpm store) as well as outside it (hoisted dependency).
546
+ + // Anchor the actual path so Vite loads the same file we found.
437
547
  + mapped = `/@fs${resolved.replace(/\\/g, '/')}${querySuffix}`;
438
548
  }
439
549
  }
@@ -475,6 +585,80 @@ index a11fbd5d24579cdf572a91f70cb06561bfa43cd0..c887334efce2c2f6cf85c75082761094
475
585
  }
476
586
  }
477
587
  return null;
588
+ diff --git a/hmr/server/websocket-ns-m.js b/hmr/server/websocket-ns-m.js
589
+ index 63b5d9a4886d8da03b8e7deac8a16d53347ea5d7..0f92b3377e6d697f5e5c9eafad130d9579078d93 100644
590
+ --- a/hmr/server/websocket-ns-m.js
591
+ +++ b/hmr/server/websocket-ns-m.js
592
+ @@ -97,7 +97,7 @@ export function registerNsModuleServerRoute(server, options) {
593
+ // internal virtual modules (e.g., \0nsvite:nsconfig-json for
594
+ // ~/package.json) that don't exist on disk and must NOT run
595
+ // through the device post-processing pipeline below.
596
+ - if (spec.startsWith('/@id/')) {
597
+ + if (spec.startsWith('/@id/') && !CSS_MODULE_RE.test(spec)) {
598
+ try {
599
+ // First try Vite's transform pipeline directly
600
+ const vr = await sharedTransformRequest(spec);
601
+ @@ -157,8 +157,21 @@ export function registerNsModuleServerRoute(server, options) {
602
+ // Workspace css outside the vite root must be anchored to /@fs or
603
+ // `?inline` is denied and the DOM css module (__vite__updateStyle)
604
+ // is served instead.
605
+ - const inlineSpec = resolveFsAnchoredSpec(spec, serverRoot, monorepoWorkspaceRoot) || spec;
606
+ - const inlineRes = await sharedTransformRequest(`${inlineSpec}?inline`);
607
+ + const inlineSpec = spec.startsWith('/@id/')
608
+ + ? spec.slice('/@id/'.length).replace(/__x00__/g, '\0')
609
+ + : resolveFsAnchoredSpec(spec, serverRoot, monorepoWorkspaceRoot) || spec;
610
+ + // Keep the canonical sheet's watch edges and CSS HMR boundary alive.
611
+ + // The inline variant alone is not the node stylesheet plugins reload.
612
+ + const virtualCss = spec.startsWith('/@id/');
613
+ + // Plugin reloads can invalidate only the canonical sheet, not its
614
+ + // inline sibling or the NativeScript transform-result TTL cache.
615
+ + const compileCss = virtualCss ? (url) => server.transformRequest(url) : sharedTransformRequest;
616
+ + if (virtualCss) {
617
+ + await compileCss(inlineSpec);
618
+ + const inlineModule = server.moduleGraph.getModuleById(`${inlineSpec}?inline`);
619
+ + if (inlineModule) server.moduleGraph.invalidateModule(inlineModule);
620
+ + }
621
+ + const inlineRes = await compileCss(`${inlineSpec}?inline`);
622
+ const inlineCode = inlineRes?.code || '';
623
+ // `?inline` output is `export default "<compiled css>"`.
624
+ const m = inlineCode.match(/export\s+default\s+("(?:[^"\\]|\\.)*")\s*;?/s);
625
+ @@ -168,7 +181,14 @@ export function registerNsModuleServerRoute(server, options) {
626
+ css = JSON.parse(m[1]);
627
+ }
628
+ catch { }
629
+ - css = normalizeCssForDevice(css);
630
+ + css = normalizeCssForDevice(css) || '/* empty stylesheet */';
631
+ + // HMR fetches adapted text through this same compiler and tag.
632
+ + if (urlObj.searchParams.has('direct')) {
633
+ + res.setHeader('Content-Type', 'text/css; charset=utf-8');
634
+ + res.statusCode = 200;
635
+ + res.end(css);
636
+ + return;
637
+ + }
638
+ const out = `// [ns:m][css] ${spec}\n` + `const __ns_css_text__ = ${JSON.stringify(css)};\n` + buildCssRegisterSnippetFromVar(spec, '__ns_css_text__') + `export default __ns_css_text__;\n`;
639
+ res.statusCode = 200;
640
+ res.end(out);
641
+ @@ -181,7 +201,10 @@ export function registerNsModuleServerRoute(server, options) {
642
+ if (verbose)
643
+ console.warn('[ns:m][css] inline compile failed', spec, cssErr?.message);
644
+ }
645
+ - // Fall through to default handling if inline compile produced nothing.
646
+ + // Never send raw CSS or a DOM injection module to the native loader.
647
+ + res.statusCode = 500;
648
+ + res.end('throw new Error(' + JSON.stringify('[ns:m] CSS compilation failed: ' + spec) + ');');
649
+ + return;
650
+ }
651
+ // Queue and dedupe transformRequest calls so heavy app graphs do not
652
+ // overwhelm Vite with concurrent work. Slow-transform warnings start only
653
+ @@ -503,6 +526,8 @@ export function registerNsModuleServerRoute(server, options) {
654
+ continue;
655
+ }
656
+ else if (u.pathname.startsWith('/ns/m')) {
657
+ + // The CSS route synthesizes a default string export after compilation.
658
+ + if (CSS_MODULE_RE.test(u.pathname)) continue;
659
+ // Resolve the import target through the SAME
660
+ // canonical pipeline the /ns/m route itself uses
661
+ // (websocket-ns-m-request.ts) instead of a third
478
662
  diff --git a/hmr/server/websocket-served-module-helpers.js b/hmr/server/websocket-served-module-helpers.js
479
663
  index b9e3ab67eb4ae7c9b12bc4b02b9aa3660d712644..f857f4a5a5956af229e447ed04bf1a52a90c6399 100644
480
664
  --- a/hmr/server/websocket-served-module-helpers.js
@@ -499,3 +683,27 @@ index b9e3ab67eb4ae7c9b12bc4b02b9aa3660d712644..f857f4a5a5956af229e447ed04bf1a52
499
683
  .map((node) => ({
500
684
  start: node.start,
501
685
  end: node.end,
686
+ diff --git a/hmr/server/websocket.js b/hmr/server/websocket.js
687
+ index e6a14437f8990e2c631f23eb8db21aa0b28b5759..2b9fb450511b6e114b6e7e7ac8a48831b4a09dc9 100644
688
+ --- a/hmr/server/websocket.js
689
+ +++ b/hmr/server/websocket.js
690
+ @@ -1,3 +1,5 @@
691
+ +import { virtualCssUpdates } from './css-device-module.js';
692
+ +import { buildCssUpdatesMessage } from './css-update-message.js';
693
+ import { readFileSync } from 'fs';
694
+ import { WebSocketServer } from 'ws';
695
+ import * as path from 'path';
696
+ @@ -341,6 +343,13 @@ function createHmrWebSocketPlugin(opts, strategy) {
697
+ // to reach the device, since they drive the
698
+ // in-place `ɵɵreplaceMetadata` template-swap path.
699
+ // Filter the relay to those.
700
+ + const cssUpdates = virtualCssUpdates(normalized);
701
+ + if (cssUpdates.length) {
702
+ + const message = JSON.stringify(buildCssUpdatesMessage(server, cssUpdates));
703
+ + wss?.clients.forEach((client) => {
704
+ + if (client?.readyState === 1) client.send(message);
705
+ + });
706
+ + }
707
+ if (normalized.type !== 'custom')
708
+ return;
709
+ // Root-component guard. Analog's in-place
@@ -0,0 +1,13 @@
1
+ diff --git a/list/index.ios.js b/list/index.ios.js
2
+ index d683fa030ea4adb1b442405458055256319e0adc..97f1953ffbc8ca9e809953c2024e17cd32beb1bb 100644
3
+ --- a/list/index.ios.js
4
+ +++ b/list/index.ios.js
5
+ @@ -42,7 +42,7 @@ var ListDelegateImpl = (function (_super) {
6
+ });
7
+ }
8
+ };
9
+ - ListDelegateImpl.ObjCProtocols = [MasonListDelegate];
10
+ + ListDelegateImpl.ObjCProtocols = typeof MasonListDelegate !== "undefined" ? [MasonListDelegate] : [];
11
+ return ListDelegateImpl;
12
+ }(NSObject));
13
+ const listState_ = Symbol('[[listState]]');
@@ -1,5 +1,5 @@
1
1
  diff --git a/dist/cjs/runtime.cjs b/dist/cjs/runtime.cjs
2
- index a035f05..9912e51 100644
2
+ index a035f05..e1c9ca1 100644
3
3
  --- a/dist/cjs/runtime.cjs
4
4
  +++ b/dist/cjs/runtime.cjs
5
5
  @@ -365,6 +365,7 @@ var import_stream_protocol = require("./stream-protocol.cjs");
@@ -18,6 +18,15 @@ index a035f05..9912e51 100644
18
18
  function useState(initial, slot) {
19
19
  const s = readStateHook(initial, slot, arguments.length === 1);
20
20
  return [s.value, s.setter];
21
+ @@ -9659,6 +9661,8 @@ function renderClientContextProvider(context, props, renderScope) {
22
+ ensureHooks(scope).set(CHILDREN_DIALECT_SLOT, dialect);
23
+ }
24
+ renderSharedBody(childrenAsBody(children), void 0, scope, void 0);
25
+ + } else {
26
+ + resetScopeChildren(scope);
27
+ }
28
+ }
29
+ let SHARED_BODY_SCOPE = null;
21
30
  diff --git a/dist/cjs/signals/client.cjs b/dist/cjs/signals/client.cjs
22
31
  index 28e2844..af40518 100644
23
32
  --- a/dist/cjs/signals/client.cjs
@@ -453,7 +462,7 @@ index 90848a6..474c2bb 100644
453
462
  + let specifiers = signalHelperImports.get(request);
454
463
  + if (specifiers === undefined) signalHelperImports.set(request, (specifiers = []));
455
464
  diff --git a/dist/node/runtime.js b/dist/node/runtime.js
456
- index 927d060..cef6b5b 100644
465
+ index 927d060..d311ac9 100644
457
466
  --- a/dist/node/runtime.js
458
467
  +++ b/dist/node/runtime.js
459
468
  @@ -185,6 +185,7 @@ import {
@@ -472,6 +481,15 @@ index 927d060..cef6b5b 100644
472
481
  function useState(initial, slot) {
473
482
  const s = readStateHook(initial, slot, arguments.length === 1);
474
483
  return [s.value, s.setter];
484
+ @@ -9532,6 +9534,8 @@ function renderClientContextProvider(context, props, renderScope) {
485
+ ensureHooks(scope).set(CHILDREN_DIALECT_SLOT, dialect);
486
+ }
487
+ renderSharedBody(childrenAsBody(children), void 0, scope, void 0);
488
+ + } else {
489
+ + resetScopeChildren(scope);
490
+ }
491
+ }
492
+ let SHARED_BODY_SCOPE = null;
475
493
  diff --git a/dist/node/signals/client.js b/dist/node/signals/client.js
476
494
  index 21a39c3..3f85700 100644
477
495
  --- a/dist/node/signals/client.js
@@ -1034,7 +1052,7 @@ index 6427a01..8049b78 100644
1034
1052
  createContext,
1035
1053
  createSubSlot,
1036
1054
  diff --git a/dist/runtime.js b/dist/runtime.js
1037
- index 67ace09..ef61a91 100644
1055
+ index 67ace09..afeb8ea 100644
1038
1056
  --- a/dist/runtime.js
1039
1057
  +++ b/dist/runtime.js
1040
1058
  @@ -184,6 +184,7 @@ import {
@@ -1053,6 +1071,15 @@ index 67ace09..ef61a91 100644
1053
1071
  function useState(initial, slot) {
1054
1072
  const s = readStateHook(initial, slot, arguments.length === 1);
1055
1073
  return [s.value, s.setter];
1074
+ @@ -9531,6 +9533,8 @@ function renderClientContextProvider(context, props, renderScope) {
1075
+ ensureHooks(scope).set(CHILDREN_DIALECT_SLOT, dialect);
1076
+ }
1077
+ renderSharedBody(childrenAsBody(children), void 0, scope, void 0);
1078
+ + } else {
1079
+ + resetScopeChildren(scope);
1080
+ }
1081
+ }
1082
+ let SHARED_BODY_SCOPE = null;
1056
1083
  diff --git a/dist/signals/client.d.ts b/dist/signals/client.d.ts
1057
1084
  index 6388c2f..f9e9862 100644
1058
1085
  --- a/dist/signals/client.d.ts