Skip to content

Commit c29c7dd

Browse files
serpentbladeclaude
andcommitted
fix(vr): lower Lit decorators via esbuild pre-transform under vite 8
Vite 8 transforms with Oxc, not esbuild, so the original D-VR-01 `esbuild.tsconfigRaw` decorator config was ignored and Lit's `@customElement`/`@property`/`@queryAssignedElements` decorators leaked raw → `SyntaxError` at load → blank Lit cells (the bulk of the failed Linux VR run). Other 5 targets emit no decorators, so were unaffected. Oxc's `decorator.legacy` path is NOT a faithful substitute: it fixes the simple cells but DROPS the getter for `@queryAssignedElements({slot})` initializer-less fields, so the Rozie-emitted `_armListeners()` slot wiring reads `undefined.length` and the heavy Lit cells (code-mirror, chart, tiptap, command-palette, data-table grids, *-virtual) crash at `firstUpdated`. Instead, lower the Lit `.rozie.ts` virtual modules with esbuild — the exact, proven Vite ≤7 path — via an `enforce:'pre'` plugin, before Oxc sees them. esbuild emits decorator-free JS so Oxc's pass is a no-op. Scoped to the Lit sub-build's decorator-bearing modules only. Verified: fresh dist/lit has zero raw-decorator leak; Card, Counter, CodeMirror, Chart, TipTap, CommandPalette all mount with populated shadow DOM and no console errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KSYH6VBAJwa7nYy4AksuNH
1 parent e6b05d9 commit c29c7dd

1 file changed

Lines changed: 45 additions & 23 deletions

File tree

tests/visual-regression/vite.config.ts

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defineConfig, type Plugin } from 'vite';
1+
import { defineConfig, transformWithEsbuild, type Plugin } from 'vite';
22
import { dirname, resolve } from 'node:path';
33
import { readFileSync } from 'node:fs';
44
import { createRequire } from 'node:module';
@@ -112,6 +112,45 @@ function resolveCrossTreeBareImports(extraRoots: readonly string[]): Plugin {
112112
};
113113
}
114114

115+
/**
116+
* D-VR-01 (Vite 8): Lit's `@rozie/target-lit` emit uses legacy class-field
117+
* decorators (`@customElement('rozie-…') class`, `@property()`, `@state()`,
118+
* `@queryAssignedElements({slot})`). Vite 8 transforms with Oxc, whose legacy-
119+
* decorator lowering is NOT faithful to esbuild's: it drops the getter for
120+
* `@queryAssignedElements`-style decorated, initializer-less fields, so the
121+
* Rozie-emitted `_armListeners()` slot wiring reads `undefined.length` and the
122+
* heavy Lit cells crash at `firstUpdated`. Rather than chase Oxc's decorator
123+
* semantics, lower the Lit virtual modules with esbuild — the exact, proven
124+
* Vite ≤7 path — BEFORE Oxc sees them. `enforce: 'pre'` runs this ahead of the
125+
* built-in Oxc transform; esbuild emits decorator-free JS, so Oxc's pass is a
126+
* no-op on the result. Scoped to the Lit sub-build's `.rozie.ts` modules only.
127+
*/
128+
function lowerLitDecoratorsWithEsbuild(): Plugin {
129+
return {
130+
name: 'rozie-vr:lit-decorator-esbuild',
131+
enforce: 'pre',
132+
async transform(code, id) {
133+
const cleanId = id.split('?', 1)[0];
134+
if (!cleanId.endsWith('.rozie.ts')) return null;
135+
// Only the decorator-bearing component modules need it; skip the rest so
136+
// we don't double-transform plain helper modules.
137+
if (!/@(customElement|property|state|query|eventOptions)\b/.test(code)) {
138+
return null;
139+
}
140+
const result = await transformWithEsbuild(code, id, {
141+
loader: 'ts',
142+
tsconfigRaw: {
143+
compilerOptions: {
144+
experimentalDecorators: true,
145+
useDefineForClassFields: false,
146+
},
147+
},
148+
});
149+
return { code: result.code, map: result.map };
150+
},
151+
};
152+
}
153+
115154
async function frameworkPlugins(target: Target) {
116155
switch (target) {
117156
case 'vue': {
@@ -568,6 +607,7 @@ export default defineConfig(async () => {
568607
...(TARGET === 'angular' ? { prebuildExtraRoots: [examplesRoot, sortableListSrc, flatpickrSrc, fullCalendarSrc, codeMirrorSrc, chartSrc, tipTapSrc, mapLibreSrc, cropperSrc, pdfSrc, reteSrc, emblaSrc, listboxSrc, sliderSrc, dataTableSrc, otpSrc, dialogSrc, comboboxSrc, toastSrc, tagsSrc, numberFieldSrc, paginationSrc, switchSrc, popoverSrc, datePickerSrc, resizableSrc, commandPaletteSrc, headlessCoreSrc] } : {}),
569608
}),
570609
...(await frameworkPlugins(TARGET)),
610+
...(TARGET === 'lit' ? [lowerLitDecoratorsWithEsbuild()] : []),
571611
],
572612
// FullCalendar plugin packages ship a CJS build whose interop shape is
573613
// `exports["default"] = plugin` WITHOUT `__esModule`. When Vite's esbuild
@@ -592,28 +632,10 @@ export default defineConfig(async () => {
592632
'@fullcalendar/list',
593633
],
594634
},
595-
// D-VR-01: the `@rozie/target-lit` emitter emits TC39-stage-3 *class-field*
596-
// decorators (`@property() foo;`). esbuild — Vite's transform pipeline — does
597-
// not read the workspace `tsconfig.json` for the `.rozie.ts` virtual modules
598-
// `@rozie/unplugin` produces, so it defaulted to standard-decorator semantics
599-
// and the Lit/preact-signals runtime threw `Unsupported decorator location:
600-
// field` at class-construction time. Passing `tsconfigRaw` with
601-
// `experimentalDecorators` (and `useDefineForClassFields: false`) tells
602-
// esbuild to lower Lit's class-field decorators in the legacy form the Lit 3
603-
// decorator runtime accepts. Scoped to the Lit sub-build only — the other
604-
// five targets emit no class-field decorators.
605-
...(TARGET === 'lit'
606-
? {
607-
esbuild: {
608-
tsconfigRaw: {
609-
compilerOptions: {
610-
experimentalDecorators: true,
611-
useDefineForClassFields: false,
612-
},
613-
},
614-
},
615-
}
616-
: {}),
635+
// Lit decorator lowering for Vite 8 lives in the `lowerLitDecoratorsWithEsbuild`
636+
// plugin above (esbuild pre-transform), NOT here — Oxc's `oxc.decorator.legacy`
637+
// path is not faithful enough (drops `@queryAssignedElements` getters). See that
638+
// plugin's banner for the full rationale.
617639
build: {
618640
outDir: resolve(__dirname, 'dist', TARGET),
619641
// Each target build must NOT wipe sibling target builds.

0 commit comments

Comments
 (0)