From 864267b269c27901fb288f9e1c19c4a4431c287e Mon Sep 17 00:00:00 2001 From: Kealan Clarke <59548500+KealanAU@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:37:09 +1100 Subject: [PATCH 01/16] [fix]: add explicit type annotations, build internal package before starting dev watch --- packages/vue-lynx/main-thread/src/element-registry.ts | 2 +- packages/vue-lynx/runtime/src/TransitionGroup.ts | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/vue-lynx/main-thread/src/element-registry.ts b/packages/vue-lynx/main-thread/src/element-registry.ts index d2b88d532..b5a5af01b 100644 --- a/packages/vue-lynx/main-thread/src/element-registry.ts +++ b/packages/vue-lynx/main-thread/src/element-registry.ts @@ -3,7 +3,7 @@ // LICENSE file in the root directory of this source tree. /** Map from BG-thread ShadowElement id to Lynx Main Thread element handle */ -export const elements = new Map(); +export const elements: Map = new Map(); /** * PAPI unique ID of the root PageElement. diff --git a/packages/vue-lynx/runtime/src/TransitionGroup.ts b/packages/vue-lynx/runtime/src/TransitionGroup.ts index 67f734f19..1dfbc5aff 100644 --- a/packages/vue-lynx/runtime/src/TransitionGroup.ts +++ b/packages/vue-lynx/runtime/src/TransitionGroup.ts @@ -13,6 +13,8 @@ import { type SetupContext, + type VNode, + type Component, defineComponent, getCurrentInstance, h, @@ -126,7 +128,7 @@ export interface TransitionGroupProps extends TransitionProps { // // --------------------------------------------------------------------------- -export const TransitionGroup = defineComponent({ +export const TransitionGroup: Component = defineComponent({ name: 'TransitionGroup', props: { @@ -152,11 +154,11 @@ export const TransitionGroup = defineComponent({ moveClass: String, }, - setup(props: TransitionGroupProps, { slots }: SetupContext) { + setup(props: TransitionGroupProps, { slots }: SetupContext): () => VNode { const instance = getCurrentInstance()!; const state = useTransitionState(); - return () => { + return (): VNode => { const rawChildren = slots.default ? slots.default() : []; const children = getTransitionRawChildren(rawChildren); From 26308af95aa2b87b28afa94a3b6ddcb72f8a33cf Mon Sep 17 00:00:00 2001 From: Kealan Clarke <59548500+KealanAU@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:58:35 +1100 Subject: [PATCH 02/16] [feat] updates to have the scoped-css work --- examples/css-features/src/App.vue | 8 +- .../css-features/src/ScopedStyleWorking.vue | 59 +++ examples/css-features/src/VBindCSS.vue | 103 +++-- package.json | 2 +- .../template-vue-js/lynx.config.js | 2 + .../template-vue-ts/lynx.config.ts | 2 + packages/upstream-tests/package.json | 2 + .../upstream-tests/src/local-test-setup.ts | 71 ++++ .../upstream-tests/src/use-css-vars.spec.ts | 365 ++++++++++++++++++ packages/upstream-tests/tsconfig.json | 5 + .../upstream-tests/vitest.local.config.ts | 82 ++++ packages/vue-lynx/runtime/src/LYNX-ISSUES.md | 84 ++++ packages/vue-lynx/runtime/src/index.ts | 7 + packages/vue-lynx/runtime/src/node-ops.ts | 14 + packages/vue-lynx/runtime/src/use-css-vars.md | 137 +++++++ packages/vue-lynx/runtime/src/use-css-vars.ts | 86 +++++ 16 files changed, 998 insertions(+), 31 deletions(-) create mode 100644 examples/css-features/src/ScopedStyleWorking.vue create mode 100644 packages/upstream-tests/src/local-test-setup.ts create mode 100644 packages/upstream-tests/src/use-css-vars.spec.ts create mode 100644 packages/upstream-tests/vitest.local.config.ts create mode 100644 packages/vue-lynx/runtime/src/LYNX-ISSUES.md create mode 100644 packages/vue-lynx/runtime/src/use-css-vars.md create mode 100644 packages/vue-lynx/runtime/src/use-css-vars.ts diff --git a/examples/css-features/src/App.vue b/examples/css-features/src/App.vue index 7a9be7e81..6edf33b29 100644 --- a/examples/css-features/src/App.vue +++ b/examples/css-features/src/App.vue @@ -1,6 +1,7 @@ + + + + diff --git a/examples/css-features/src/VBindCSS.vue b/examples/css-features/src/VBindCSS.vue index 4b703613f..a1661e2a0 100644 --- a/examples/css-features/src/VBindCSS.vue +++ b/examples/css-features/src/VBindCSS.vue @@ -1,44 +1,91 @@ + + diff --git a/package.json b/package.json index a6851e14c..2e4b351ae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue-lynx-monorepo", - "private": true, + "private": false, "scripts": { "build": "pnpm --filter vue-lynx run build", "build:examples": "pnpm --filter './examples/*' --parallel run build", diff --git a/packages/create-vue-lynx/template-vue-js/lynx.config.js b/packages/create-vue-lynx/template-vue-js/lynx.config.js index 7f6efc11f..8cafb6d27 100644 --- a/packages/create-vue-lynx/template-vue-js/lynx.config.js +++ b/packages/create-vue-lynx/template-vue-js/lynx.config.js @@ -20,6 +20,8 @@ export default defineConfig({ }), pluginVueLynx({ optionsApi: false, + enableCSSInlineVariables: true, + enableCSSInheritance: true, }), ], }) diff --git a/packages/create-vue-lynx/template-vue-ts/lynx.config.ts b/packages/create-vue-lynx/template-vue-ts/lynx.config.ts index bc9c89d54..625fec73e 100644 --- a/packages/create-vue-lynx/template-vue-ts/lynx.config.ts +++ b/packages/create-vue-lynx/template-vue-ts/lynx.config.ts @@ -17,6 +17,8 @@ export default defineConfig({ }), pluginVueLynx({ optionsApi: false, + enableCSSInlineVariables: true, + enableCSSInheritance: true, }), ], }) diff --git a/packages/upstream-tests/package.json b/packages/upstream-tests/package.json index c5ac62288..540e4b198 100644 --- a/packages/upstream-tests/package.json +++ b/packages/upstream-tests/package.json @@ -8,6 +8,8 @@ "test": "vitest run", "test:dom": "vitest run --config vitest.dom.config.ts", "test:dom:watch": "vitest --config vitest.dom.config.ts", + "test:local": "vitest run --config vitest.local.config.ts", + "test:local:watch": "vitest --config vitest.local.config.ts", "test:watch": "vitest", "vuejs:init": "git submodule update --init --depth 1 core", "vuejs:status": "cd core && git log --oneline -1" diff --git a/packages/upstream-tests/src/local-test-setup.ts b/packages/upstream-tests/src/local-test-setup.ts new file mode 100644 index 000000000..381d68819 --- /dev/null +++ b/packages/upstream-tests/src/local-test-setup.ts @@ -0,0 +1,71 @@ +/** + * Minimal setup for local (non-upstream) tests that exercise the vue-lynx + * BG-thread renderer directly, without the full DOM bridge pipeline. + * + * The vue-lynx flush module sends ops to the main thread via + * `lynx.getNativeApp().callLepusMethod('vuePatchUpdate', ...)`. In the test + * environment `lynx` is not defined, so we stub it here. + * + * We also capture the JSON-serialised ops that `doFlush` passes to + * `callLepusMethod` and re-push them into a test-accessible buffer so that + * tests can inspect them via `collectFlushedOps()` after awaiting `nextTick`. + */ + +import { OP } from 'vue-lynx/internal/ops'; + +// --------------------------------------------------------------------------- +// Captured ops buffer +// --------------------------------------------------------------------------- + +// Ops that doFlush() sent to callLepusMethod (after JSON round-trip). +let _capturedOps: unknown[] = []; + +/** + * Return and clear all ops that have been flushed to the "main thread" since + * the last call. Call this after `await nextTick()` instead of `takeOps()`. + */ +export function collectFlushedOps(): unknown[] { + const ops = _capturedOps; + _capturedOps = []; + return ops; +} + +export function resetCapturedOps(): void { + _capturedOps = []; +} + +// --------------------------------------------------------------------------- +// lynx stub +// --------------------------------------------------------------------------- + +const lynxStub = { + getNativeApp() { + return { + callLepusMethod( + _method: string, + params: { data: string }, + callback: () => void, + ) { + // Deserialise and capture the ops for test assertions. + const ops: unknown[] = JSON.parse(params.data); + _capturedOps.push(...ops); + // Simulate synchronous MT acknowledgement. + callback(); + }, + }; + }, +}; + +// Install as a global so flush.ts's `declare var lynx` reference resolves. +(globalThis as Record)['lynx'] = lynxStub; + +// --------------------------------------------------------------------------- +// Per-test reset +// --------------------------------------------------------------------------- + +beforeEach(() => { + resetCapturedOps(); +}); + +// Re-export OP for convenience in tests that import from this file. +export { OP }; diff --git a/packages/upstream-tests/src/use-css-vars.spec.ts b/packages/upstream-tests/src/use-css-vars.spec.ts new file mode 100644 index 000000000..86f696a6e --- /dev/null +++ b/packages/upstream-tests/src/use-css-vars.spec.ts @@ -0,0 +1,365 @@ +/** + * Tests for useCssVars — the Lynx BG-thread implementation of Vue's + * CSS v-bind() support. + * + * useCssVars is called by the SFC compiler when a +``` + +The compiler transforms this at build time: + +- **CSS** — `color: v-bind(textColor)` becomes `color: var(--v33993c7f)` +- **JS** — a `useCssVars` call is injected into `setup()`: `useCssVars(_ctx => ({ "v33993c7f": _ctx.textColor }))` + +The hash (`v33993c7f`) is deterministic and scoped per component. + +--- + +## Setup + +Two flags must be set in `lynx.config.ts`: + +```ts +pluginVueLynx({ + enableCSSInlineVariables: true, // allows --* properties in inline styles + enableCSSInheritance: true, // allows CSS vars to cascade to child elements +}) +``` + +Without `enableCSSInlineVariables` the engine ignores `--*` on inline styles. Without `enableCSSInheritance` the variable is set on the component root but never reaches descendant elements. + +--- + +## What works + +```vue + +``` + +Paint-only properties (`color`, `background-color`, `opacity`, `border-color`) update correctly when the reactive value changes. + +--- + +## Known limitation — layout properties + +```vue + +``` + +`font-size` and other layout-affecting properties do not update visually when the reactive value changes. The CSS var is set correctly on the root element, but the Lynx engine does not appear to trigger a layout re-pass when an inherited inline CSS var changes. This is specific to dynamic updates — the initial render value is applied correctly. + +**Workaround:** drive layout properties via `:style` binding directly on the element. + +```vue +... +``` + +This is undocumented behaviour in the Lynx engine. See [`LYNX-ISSUES.md`](./LYNX-ISSUES.md) for the issue to file upstream. + +--- + +## Use ` + + + +``` + +A DEV-mode console warning is emitted when a scoped component mounts, pointing to ` -``` - -The compiler transforms this at build time: - -- **CSS** — `color: v-bind(textColor)` becomes `color: var(--v33993c7f)` -- **JS** — a `useCssVars` call is injected into `setup()`: `useCssVars(_ctx => ({ "v33993c7f": _ctx.textColor }))` - -The hash (`v33993c7f`) is deterministic and scoped per component. - ---- - -## Setup - -Two flags must be set in `lynx.config.ts`: - -```ts -pluginVueLynx({ - enableCSSInlineVariables: true, // allows --* properties in inline styles - enableCSSInheritance: true, // allows CSS vars to cascade to child elements -}) -``` - -Without `enableCSSInlineVariables` the engine ignores `--*` on inline styles. Without `enableCSSInheritance` the variable is set on the component root but never reaches descendant elements. - ---- - -## What works - -```vue - -``` - -Paint-only properties (`color`, `background-color`, `opacity`, `border-color`) update correctly when the reactive value changes. - ---- - -## Known limitation — layout properties - -```vue - -``` - -`font-size` and other layout-affecting properties do not update visually when the reactive value changes. The CSS var is set correctly on the root element, but the Lynx engine does not appear to trigger a layout re-pass when an inherited inline CSS var changes. This is specific to dynamic updates — the initial render value is applied correctly. - -**Workaround:** drive layout properties via `:style` binding directly on the element. - -```vue -... -``` - -This is undocumented behaviour in the Lynx engine. See [`LYNX-ISSUES.md`](./LYNX-ISSUES.md) for the issue to file upstream. - ---- - -## Use ` - - - -``` - -A DEV-mode console warning is emitted when a scoped component mounts, pointing to ` diff --git a/examples/css-features/src/VBindCSS.vue b/examples/css-features/src/VBindCSS.vue index a1661e2a0..a865ba2a0 100644 --- a/examples/css-features/src/VBindCSS.vue +++ b/examples/css-features/src/VBindCSS.vue @@ -1,9 +1,21 @@ @@ -80,12 +134,33 @@ function cycleFontSize() { .sample-text { color: v-bind(textColor); - /* font-size: v-bind(fontSize) is omitted here. - font-size IS in Lynx's default CSS inheritance list and var() in CSS - class rules is supported, but dynamic CSS var updates on inline styles - do not appear to trigger a layout re-pass for font-size (unlike color, - which is paint-only). This gap is undocumented in Lynx and no roadmap - item exists for it. font-size is driven via :style binding instead. */ font-weight: bold; + margin-bottom: 4px; +} + +.computed-text { + color: v-bind(invertedColor); + font-weight: bold; + margin-bottom: 4px; +} + +.border-text { + color: v-bind('borderStyle.color'); + font-weight: bold; + margin-bottom: 4px; +} + +.opacity-text { + color: v-bind(textColor); + opacity: v-bind(opacity); + font-weight: bold; + margin-bottom: 4px; +} + +.var-text { + --accent: v-bind(textColor); + color: var(--accent); + font-weight: bold; + margin-bottom: 8px; } diff --git a/examples/css-features/src/VBindThreads.vue b/examples/css-features/src/VBindThreads.vue new file mode 100644 index 000000000..03defa1f0 --- /dev/null +++ b/examples/css-features/src/VBindThreads.vue @@ -0,0 +1,129 @@ + + + + + diff --git a/packages/vue-lynx/runtime/src/node-ops.ts b/packages/vue-lynx/runtime/src/node-ops.ts index 5a3ffd813..1ef478bee 100644 --- a/packages/vue-lynx/runtime/src/node-ops.ts +++ b/packages/vue-lynx/runtime/src/node-ops.ts @@ -309,20 +309,6 @@ export const nodeOps: RendererOptions = { nextSibling(node: ShadowElement): ShadowElement | null { return node.next; }, - - // Vue calls setScopeId when mounting a component that uses diff --git a/examples/css-features/src/VBindRow.vue b/examples/css-features/src/VBindRow.vue new file mode 100644 index 000000000..7786b6fa7 --- /dev/null +++ b/examples/css-features/src/VBindRow.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/examples/css-features/src/VBindThreads.vue b/examples/css-features/src/VBindThreads.vue index 03defa1f0..c712a37f6 100644 --- a/examples/css-features/src/VBindThreads.vue +++ b/examples/css-features/src/VBindThreads.vue @@ -1,53 +1,25 @@ @@ -55,67 +27,54 @@ const onMtTap = () => { - - Thread comparison — style updates + + Inline :style + var()  vs  v-bind() in CSS - - Both boxes cycle colors on tap. BG path uses v-bind() in CSS; MT path calls setStyleProperty() directly. + + Same reactive ref. Tap either box to cycle — compare how each approach updates. - + - Background thread + :style + var() - Taps: {{ bgTapCount }} + Taps: {{ tapCount }} - v-bind(bgColor) in CSS + background-color: var(--box-color) - + - Main thread + v-bind() in CSS - Taps: {{ mtTapCount }} + Taps: {{ tapCount }} - setStyleProperty() on MT + background-color: v-bind(color) @@ -123,7 +82,11 @@ const onMtTap = () => { From db4824626307eba4a38306d87df1ec62ad675fc7 Mon Sep 17 00:00:00 2001 From: Kealan Clarke <59548500+KealanAU@users.noreply.github.com> Date: Sun, 5 Apr 2026 11:49:36 +1000 Subject: [PATCH 10/16] [debug] looking into why mobile doesn't get the same styling as web --- examples/css-features/src/App.vue | 14 +++++++++----- packages/vue-lynx/plugin/src/entry.ts | 6 ++++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/examples/css-features/src/App.vue b/examples/css-features/src/App.vue index 4f1a27d18..465341f19 100644 --- a/examples/css-features/src/App.vue +++ b/examples/css-features/src/App.vue @@ -26,11 +26,15 @@ import ImportedCSS from './ImportedCSS.vue' Other CSS patterns - + - - - - + + + + + + + + diff --git a/packages/vue-lynx/plugin/src/entry.ts b/packages/vue-lynx/plugin/src/entry.ts index 693f038d4..0dc3755f4 100644 --- a/packages/vue-lynx/plugin/src/entry.ts +++ b/packages/vue-lynx/plugin/src/entry.ts @@ -175,9 +175,11 @@ class VueCSSConfigPlugin { }; hooks.beforeEncode.tap(PLUGIN_CSS_CONFIG, (args) => { const encodeData = args['encodeData'] as { - compilerOptions: Record; + sourceContent: { + config: Record; + }; }; - Object.assign(encodeData.compilerOptions, this.compilerOptions); + Object.assign(encodeData.sourceContent.config, this.compilerOptions); return args; }); }, From b78cc643f08872150221c3f8df48d266ff7f7a07 Mon Sep 17 00:00:00 2001 From: Kealan Clarke <59548500+KealanAU@users.noreply.github.com> Date: Sun, 5 Apr 2026 13:48:05 +1000 Subject: [PATCH 11/16] [refactor] updates to the css features, change to have explict naming in title --- examples/css-features/src/CSSModules.vue | 2 +- .../css-features/src/CSSVarsWorkaround.vue | 2 +- examples/css-features/src/ImportedCSS.vue | 2 +- examples/css-features/src/InlineStyleVar.vue | 52 +++++++++++++++++++ examples/css-features/src/PlainStyle.vue | 2 +- examples/css-features/src/ScopedStyle.vue | 2 +- examples/css-features/src/VBindCSS.vue | 2 +- examples/css-features/src/VBindCSSSimple.vue | 49 +++++++++++++++++ examples/css-features/src/VBindThreads.vue | 2 +- 9 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 examples/css-features/src/InlineStyleVar.vue create mode 100644 examples/css-features/src/VBindCSSSimple.vue diff --git a/examples/css-features/src/CSSModules.vue b/examples/css-features/src/CSSModules.vue index aa4b7161a..ab8385ccf 100644 --- a/examples/css-features/src/CSSModules.vue +++ b/examples/css-features/src/CSSModules.vue @@ -17,7 +17,7 @@