@pyreon/compiler 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/README.md +17 -0
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +1189 -30
- package/lib/types/index.d.ts +109 -2
- package/package.json +20 -2
- package/src/event-names.ts +65 -0
- package/src/index.ts +17 -0
- package/src/island-audit.ts +675 -0
- package/src/jsx.ts +162 -39
- package/src/load-native.ts +155 -0
- package/src/pyreon-intercept.ts +352 -2
- package/src/ssg-audit.ts +513 -0
- package/src/tests/detector-tag-consistency.test.ts +31 -15
- package/src/tests/island-audit.test.ts +524 -0
- package/src/tests/jsx.test.ts +236 -4
- package/src/tests/load-native.test.ts +53 -0
- package/src/tests/native-equivalence.test.ts +77 -0
- package/src/tests/pyreon-intercept.test.ts +296 -0
- package/src/tests/runtime/control-flow.test.ts +159 -0
- package/src/tests/runtime/dom-properties.test.ts +138 -0
- package/src/tests/runtime/events.test.ts +301 -0
- package/src/tests/runtime/harness.ts +94 -0
- package/src/tests/runtime/pr-352-shapes.test.ts +121 -0
- package/src/tests/runtime/reactive-props.test.ts +81 -0
- package/src/tests/runtime/signals.test.ts +129 -0
- package/src/tests/runtime/whitespace.test.ts +106 -0
- package/src/tests/ssg-audit.test.ts +402 -0
- package/lib/index.js.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
package/src/tests/jsx.test.ts
CHANGED
|
@@ -580,6 +580,39 @@ describe('JSX transform — template emission', () => {
|
|
|
580
580
|
expect(result).toContain('__ev_click = handler')
|
|
581
581
|
})
|
|
582
582
|
|
|
583
|
+
// Regression: multi-word event-name casing was broken — `onKeyDown`
|
|
584
|
+
// produced `addEventListener("keyDown", ...)` (camelCase) instead of
|
|
585
|
+
// `addEventListener("keydown", ...)` (DOM convention). The handler
|
|
586
|
+
// never fired because `keyDown` is not a real DOM event name.
|
|
587
|
+
// Same bug class affected `onMouseEnter`, `onMouseLeave`, etc.
|
|
588
|
+
test('lowercases multi-word event names (onKeyDown → keydown — delegated)', () => {
|
|
589
|
+
const result = t('<div><input onKeyDown={handler} /></div>')
|
|
590
|
+
// keydown IS in DELEGATED_EVENTS — must use the expando, not addEventListener.
|
|
591
|
+
// Prior behavior: addEventListener("keyDown", ...) — wrong casing AND
|
|
592
|
+
// wrong path (delegated check missed because case mismatched).
|
|
593
|
+
expect(result).toContain('__ev_keydown = handler')
|
|
594
|
+
expect(result).not.toContain('__ev_keyDown')
|
|
595
|
+
expect(result).not.toContain('"keyDown"')
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
test('lowercases multi-word event names (onMouseEnter → mouseenter — non-delegated)', () => {
|
|
599
|
+
const result = t('<div><span onMouseEnter={handler}>hi</span></div>')
|
|
600
|
+
// mouseenter is NOT delegated — must reach addEventListener with lowercase name
|
|
601
|
+
expect(result).toContain('addEventListener("mouseenter", handler)')
|
|
602
|
+
expect(result).not.toContain('"mouseEnter"')
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
test('lowercases multi-word event names for input change (onChange → change — delegated)', () => {
|
|
606
|
+
const result = t('<div><input onChange={handler} /></div>')
|
|
607
|
+
expect(result).toContain('__ev_change = handler')
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
test('lowercases multi-word event names with multiple capitals (onPointerLeave → pointerleave)', () => {
|
|
611
|
+
const result = t('<div><span onPointerLeave={handler}>hi</span></div>')
|
|
612
|
+
expect(result).toContain('addEventListener("pointerleave", handler)')
|
|
613
|
+
expect(result).not.toContain('"pointerLeave"')
|
|
614
|
+
})
|
|
615
|
+
|
|
583
616
|
test('uses element children indexing for nested access', () => {
|
|
584
617
|
const result = t('<div><span>{a()}</span><em>{b()}</em></div>')
|
|
585
618
|
// Can't have two expression children in same parent, but each is in its own element
|
|
@@ -779,6 +812,55 @@ describe('JSX transform — template emission', () => {
|
|
|
779
812
|
expect(result).toContain('((el) => { myEl = el })(__root)')
|
|
780
813
|
})
|
|
781
814
|
|
|
815
|
+
test('block-arrow ref on a child element with adjacent reactive props compiles cleanly', () => {
|
|
816
|
+
// Regression: a child element (NOT __root) with `hasDynamic=true`
|
|
817
|
+
// used to emit `const __e0 = __root.children[N]` followed by an
|
|
818
|
+
// unterminated ref-call `((el) => { x = el })(__e0)`. Without a
|
|
819
|
+
// trailing `;` on the const line, ASI did NOT insert one (because
|
|
820
|
+
// `__root.children[N]((el) => ...)` is a valid function call), and
|
|
821
|
+
// the two lines parsed as ONE expression:
|
|
822
|
+
// `const __e0 = __root.children[N]((el) => ...)(__e0)`
|
|
823
|
+
// — calling `children[N]` as a function with the arrow as arg, and
|
|
824
|
+
// self-referencing `__e0` before assignment. Surfaced when the
|
|
825
|
+
// app-showcase /dnd demo used `ref={(el) => { letVar = el }}` next
|
|
826
|
+
// to `data-X={signal()}` reactive props on the same element. Fix:
|
|
827
|
+
// append `;` to every bind line (`bindLines.map(l => ` ${l};`)`).
|
|
828
|
+
//
|
|
829
|
+
// This test asserts the OUTPUT is well-formed: the const line ends
|
|
830
|
+
// in `;` and the ref call IIFE follows on its own line.
|
|
831
|
+
const result = t(
|
|
832
|
+
'<div><span ref={(el) => { x = el }} data-state={cls()} /></div>',
|
|
833
|
+
)
|
|
834
|
+
// Const declaration must terminate before the ref IIFE.
|
|
835
|
+
expect(result).toMatch(/const __e0 = __root\.children\[0\];\s*\n/)
|
|
836
|
+
// The ref IIFE is its own statement, calling __e0 (not chained).
|
|
837
|
+
expect(result).toContain('((el) => { x = el })(__e0)')
|
|
838
|
+
// And the chained-call shape MUST NOT appear (the bug pattern).
|
|
839
|
+
expect(result).not.toMatch(/__root\.children\[0\]\(\(/)
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
test('compiled output parses cleanly for block-arrow ref + reactive prop', () => {
|
|
843
|
+
// Functional regression: prove the compiled module is well-formed JS.
|
|
844
|
+
// Pre-fix the AST-level shape was malformed and execution threw
|
|
845
|
+
// "TypeError: __root.children[N] is not a function" at mount time
|
|
846
|
+
// because the const declaration chained into the ref IIFE.
|
|
847
|
+
//
|
|
848
|
+
// Strip imports, stub framework calls, and parse the body via the
|
|
849
|
+
// Function constructor. If the const line lacks its terminator, the
|
|
850
|
+
// ref-call IIFE would silently turn the RHS into a function call —
|
|
851
|
+
// valid JS, wrong runtime behavior. So we BOTH parse-check AND
|
|
852
|
+
// string-shape-check: parse to catch syntax errors, regex to catch
|
|
853
|
+
// the silent-merge case (which parses fine but means the wrong thing).
|
|
854
|
+
const result = t(
|
|
855
|
+
'<div><span ref={(el) => { x = el }} data-state={cls()} /></div>',
|
|
856
|
+
)
|
|
857
|
+
const codeOnly = result.replace(/^\s*import\b[^;]+;?\s*/gm, '')
|
|
858
|
+
const wrapped = `let x;\nconst _tpl = () => {}; const _bind = () => {}; const _bindDirect = () => {}; const cls = () => "v";\nreturn ${codeOnly};`
|
|
859
|
+
expect(() => new Function(wrapped)).not.toThrow()
|
|
860
|
+
// And the buggy chained-call shape MUST NOT appear.
|
|
861
|
+
expect(result).not.toMatch(/children\[0\]\(\(/)
|
|
862
|
+
})
|
|
863
|
+
|
|
782
864
|
test('handles non-void self-closing element as closing tag', () => {
|
|
783
865
|
const result = t('<div><span></span></div>')
|
|
784
866
|
expect(result).toContain('_tpl(')
|
|
@@ -1116,13 +1198,28 @@ describe('JSX transform — template emission edge cases', () => {
|
|
|
1116
1198
|
test('non-delegated event (onMouseEnter) uses addEventListener not delegation', () => {
|
|
1117
1199
|
const result = t('<div onMouseEnter={handler}><span /></div>')
|
|
1118
1200
|
expect(result).toContain('_tpl(')
|
|
1119
|
-
// mouseenter is NOT in DELEGATED_EVENTS → must use addEventListener
|
|
1120
|
-
//
|
|
1121
|
-
|
|
1122
|
-
|
|
1201
|
+
// mouseenter is NOT in DELEGATED_EVENTS → must use addEventListener.
|
|
1202
|
+
// The event name is the JSX attribute with the "on" prefix dropped
|
|
1203
|
+
// and the rest fully lowercased (`onMouseEnter` → `mouseenter`)
|
|
1204
|
+
// — DOM events are all-lowercase. Prior to the fix this emitted
|
|
1205
|
+
// `mouseEnter` (camelCase) which the browser never dispatches.
|
|
1206
|
+
expect(result).toContain('addEventListener("mouseenter"')
|
|
1207
|
+
expect(result).not.toContain('mouseEnter')
|
|
1123
1208
|
expect(result).not.toContain('__ev_')
|
|
1124
1209
|
})
|
|
1125
1210
|
|
|
1211
|
+
// Regression: `onDoubleClick` is the one React→DOM event-name where
|
|
1212
|
+
// the simple-lowercase rule is wrong. The DOM event is `dblclick`,
|
|
1213
|
+
// NOT `doubleclick`. Pre-fix the compiler emitted `doubleclick`,
|
|
1214
|
+
// attaching a listener the browser never fires. Proven broken in
|
|
1215
|
+
// real Chromium via `e2e/app.spec.ts:dbl-click button increments by 10`.
|
|
1216
|
+
test('onDoubleClick maps to dblclick (not doubleclick)', () => {
|
|
1217
|
+
const result = t('<div onDoubleClick={handler}><span /></div>')
|
|
1218
|
+
expect(result).toContain('_tpl(')
|
|
1219
|
+
expect(result).toContain('__ev_dblclick = handler')
|
|
1220
|
+
expect(result).not.toContain('doubleclick')
|
|
1221
|
+
})
|
|
1222
|
+
|
|
1126
1223
|
test('template with both dynamic attribute AND dynamic child text', () => {
|
|
1127
1224
|
const result = t('<div title={getTitle()}>{count()}</div>')
|
|
1128
1225
|
expect(result).toContain('_tpl(')
|
|
@@ -1849,6 +1946,92 @@ describe('JSX transform — signal auto-call', () => {
|
|
|
1849
1946
|
expect(result).toContain('props.label')
|
|
1850
1947
|
})
|
|
1851
1948
|
|
|
1949
|
+
// Regression: signal-method calls were getting double-wrapped — the
|
|
1950
|
+
// auto-call inserted `()` after the bare signal reference inside
|
|
1951
|
+
// `signal.set(value)`, producing `signal().set(value)`. That calls
|
|
1952
|
+
// the signal (returns its current value, e.g. a string) then tries
|
|
1953
|
+
// `.set` on the string (undefined → TypeError). Every `signal.set`,
|
|
1954
|
+
// `signal.peek`, `signal.update` call inside event handlers / hot
|
|
1955
|
+
// paths was silently broken.
|
|
1956
|
+
test('signal.set() in event handler does NOT auto-call the bare signal reference', () => {
|
|
1957
|
+
const result = t(
|
|
1958
|
+
'function C() { const value = signal(""); return <input onInput={(e) => value.set(e.target.value)} /> }',
|
|
1959
|
+
)
|
|
1960
|
+
// Must keep `value.set(...)` — NOT `value().set(...)`.
|
|
1961
|
+
expect(result).toContain('value.set(e.target.value)')
|
|
1962
|
+
expect(result).not.toContain('value().set')
|
|
1963
|
+
})
|
|
1964
|
+
|
|
1965
|
+
test('signal.peek() does NOT auto-call', () => {
|
|
1966
|
+
const result = t(
|
|
1967
|
+
'function C() { const count = signal(0); return <button onClick={() => console.log(count.peek())}>x</button> }',
|
|
1968
|
+
)
|
|
1969
|
+
expect(result).toContain('count.peek()')
|
|
1970
|
+
expect(result).not.toContain('count().peek')
|
|
1971
|
+
})
|
|
1972
|
+
|
|
1973
|
+
test('signal.update() does NOT auto-call', () => {
|
|
1974
|
+
const result = t(
|
|
1975
|
+
'function C() { const count = signal(0); return <button onClick={() => count.update(n => n + 1)}>+</button> }',
|
|
1976
|
+
)
|
|
1977
|
+
expect(result).toContain('count.update(')
|
|
1978
|
+
expect(result).not.toContain('count().update')
|
|
1979
|
+
})
|
|
1980
|
+
|
|
1981
|
+
// Bare member-access on a signal (no call) STILL auto-calls — preserves
|
|
1982
|
+
// the existing convention where a signal containing an object can be
|
|
1983
|
+
// dereferenced via `signalContainingObj.someProp` (compiles to
|
|
1984
|
+
// `signalContainingObj().someProp`). Only the CALLED form
|
|
1985
|
+
// (`signal.method(...)`) skips the auto-call. See findSignalIdents
|
|
1986
|
+
// in jsx.ts for the rationale.
|
|
1987
|
+
test('signal.someProp (bare member access) DOES auto-call', () => {
|
|
1988
|
+
const result = t(
|
|
1989
|
+
'function C() { const data = signal({ count: 0 }); return <div>{data.count}</div> }',
|
|
1990
|
+
)
|
|
1991
|
+
expect(result).toContain('data().count')
|
|
1992
|
+
})
|
|
1993
|
+
|
|
1994
|
+
// The bare signal reference STILL auto-calls in JSX text — make sure
|
|
1995
|
+
// the fix doesn't over-correct.
|
|
1996
|
+
test('bare signal in JSX text still auto-calls (fix does not over-correct)', () => {
|
|
1997
|
+
const result = t('function C() { const count = signal(0); return <div>{count}</div> }')
|
|
1998
|
+
expect(result).toContain('count()')
|
|
1999
|
+
})
|
|
2000
|
+
|
|
2001
|
+
// ── JSX text/expression whitespace (regression) ─────────────────────
|
|
2002
|
+
// The compiler used `.replace(/\n\s*/g, '').trim()` on JSX text which
|
|
2003
|
+
// stripped ALL leading/trailing whitespace — even spaces adjacent to
|
|
2004
|
+
// expressions on the same line. So `<p>doubled: {x}</p>` produced
|
|
2005
|
+
// `<p>doubled:</p>` + appended text node, rendering "doubled:0"
|
|
2006
|
+
// instead of "doubled: 0". Same class for `<p>{x} remaining</p>` →
|
|
2007
|
+
// text "remaining" loses its leading space, rendering as "Xremaining".
|
|
2008
|
+
// Fix: only strip whitespace adjacent to newlines (multi-line JSX
|
|
2009
|
+
// formatting), preserve same-line whitespace adjacent to expressions.
|
|
2010
|
+
test('preserves trailing space in JSX text before expression on same line', () => {
|
|
2011
|
+
const result = t('<p>doubled: {x()}</p>')
|
|
2012
|
+
// The static text portion of the template must keep "doubled: "
|
|
2013
|
+
// (with trailing space) so the appended expression value renders
|
|
2014
|
+
// as "doubled: 0", not "doubled:0".
|
|
2015
|
+
expect(result).toContain('doubled: ')
|
|
2016
|
+
})
|
|
2017
|
+
|
|
2018
|
+
test('preserves leading space in JSX text after expression on same line', () => {
|
|
2019
|
+
const result = t('<p>{x()} remaining</p>')
|
|
2020
|
+
// Static portion must include " remaining" (with leading space).
|
|
2021
|
+
expect(result).toContain(' remaining')
|
|
2022
|
+
})
|
|
2023
|
+
|
|
2024
|
+
test('strips multi-line JSX text whitespace adjacent to newlines', () => {
|
|
2025
|
+
// Multi-line JSX with indentation should still collapse — only
|
|
2026
|
+
// SAME-LINE whitespace adjacent to expressions is preserved.
|
|
2027
|
+
const result = t(`<div>
|
|
2028
|
+
<span>hello</span>
|
|
2029
|
+
</div>`)
|
|
2030
|
+
// The newlines + indentation should not produce stray text nodes.
|
|
2031
|
+
expect(result).toContain('hello')
|
|
2032
|
+
expect(result).not.toContain('"\\n "')
|
|
2033
|
+
})
|
|
2034
|
+
|
|
1852
2035
|
test('shadowed signal variable by const is NOT auto-called', () => {
|
|
1853
2036
|
const result = t(`
|
|
1854
2037
|
function App() {
|
|
@@ -2066,6 +2249,55 @@ describe('JSX transform — template reactive style _bindDirect path', () => {
|
|
|
2066
2249
|
})
|
|
2067
2250
|
})
|
|
2068
2251
|
|
|
2252
|
+
// ── DOM-property assignment for value/checked/etc. (regression) ─────────
|
|
2253
|
+
// The compiler used `setAttribute("value", v)` for ALL non-class/style
|
|
2254
|
+
// attributes. For inputs that's wrong: `value` is a live DOM property,
|
|
2255
|
+
// `setAttribute` only sets the initial attribute. After the user types,
|
|
2256
|
+
// the property and attribute drift. Then `input.set('')` runs the
|
|
2257
|
+
// _bindDirect updater — which only resets the attribute, leaving the
|
|
2258
|
+
// stale typed text in the visible field. Same for `checked` on
|
|
2259
|
+
// checkboxes (presence of the attribute means checked, regardless of
|
|
2260
|
+
// value). Fix: emit property assignment for known DOM properties.
|
|
2261
|
+
describe('JSX transform — DOM properties use property assignment', () => {
|
|
2262
|
+
test('reactive value on input emits property assignment, not setAttribute', () => {
|
|
2263
|
+
const result = t('<div><input value={() => input()} /></div>')
|
|
2264
|
+
// Should be `el.value = v`, not `setAttribute("value", ...)`
|
|
2265
|
+
expect(result).toContain('.value = v')
|
|
2266
|
+
expect(result).not.toContain('setAttribute("value"')
|
|
2267
|
+
})
|
|
2268
|
+
|
|
2269
|
+
test('reactive checked on input emits property assignment', () => {
|
|
2270
|
+
const result = t('<div><input checked={done()} /></div>')
|
|
2271
|
+
expect(result).toContain('.checked = v')
|
|
2272
|
+
expect(result).not.toContain('setAttribute("checked"')
|
|
2273
|
+
})
|
|
2274
|
+
|
|
2275
|
+
test('static-call value on input emits property assignment', () => {
|
|
2276
|
+
// Non-signal-direct dynamic expression goes through reactiveBindExprs
|
|
2277
|
+
const result = t('<div><input value={x.y} /></div>')
|
|
2278
|
+
expect(result).toContain('.value = x.y')
|
|
2279
|
+
expect(result).not.toContain('setAttribute("value"')
|
|
2280
|
+
})
|
|
2281
|
+
|
|
2282
|
+
test('selected on option emits property assignment', () => {
|
|
2283
|
+
const result = t('<div><option selected={isSelected()}>x</option></div>')
|
|
2284
|
+
expect(result).toContain('.selected = v')
|
|
2285
|
+
expect(result).not.toContain('setAttribute("selected"')
|
|
2286
|
+
})
|
|
2287
|
+
|
|
2288
|
+
test('disabled on button emits property assignment', () => {
|
|
2289
|
+
const result = t('<div><button disabled={isDisabled()}>x</button></div>')
|
|
2290
|
+
expect(result).toContain('.disabled = v')
|
|
2291
|
+
expect(result).not.toContain('setAttribute("disabled"')
|
|
2292
|
+
})
|
|
2293
|
+
|
|
2294
|
+
test('non-DOM-prop attribute still uses setAttribute', () => {
|
|
2295
|
+
// placeholder is a real attribute, not a property-divergent IDL prop
|
|
2296
|
+
const result = t('<div><input placeholder={msg()} /></div>')
|
|
2297
|
+
expect(result).toContain('setAttribute("placeholder"')
|
|
2298
|
+
})
|
|
2299
|
+
})
|
|
2300
|
+
|
|
2069
2301
|
describe('JSX transform — template combined _bind for complex expressions', () => {
|
|
2070
2302
|
test('complex attribute expression uses combined _bind', () => {
|
|
2071
2303
|
const result = t('<div class={`${a()} ${b()}`}><span /></div>')
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { getPlatformPackageName } from '../load-native'
|
|
3
|
+
|
|
4
|
+
describe('getPlatformPackageName', () => {
|
|
5
|
+
it('returns @pyreon/compiler-darwin-arm64 on Apple Silicon', () => {
|
|
6
|
+
expect(getPlatformPackageName('darwin', 'arm64', null)).toBe('@pyreon/compiler-darwin-arm64')
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('returns @pyreon/compiler-darwin-x64 on Intel Mac', () => {
|
|
10
|
+
expect(getPlatformPackageName('darwin', 'x64', null)).toBe('@pyreon/compiler-darwin-x64')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('returns @pyreon/compiler-linux-x64-gnu on glibc Linux x64', () => {
|
|
14
|
+
expect(getPlatformPackageName('linux', 'x64', 'gnu')).toBe('@pyreon/compiler-linux-x64-gnu')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('returns @pyreon/compiler-linux-arm64-gnu on glibc Linux ARM64', () => {
|
|
18
|
+
expect(getPlatformPackageName('linux', 'arm64', 'gnu')).toBe('@pyreon/compiler-linux-arm64-gnu')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('returns @pyreon/compiler-win32-x64-msvc on Windows x64', () => {
|
|
22
|
+
expect(getPlatformPackageName('win32', 'x64', 'msvc')).toBe('@pyreon/compiler-win32-x64-msvc')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('returns null for unsupported platform (freebsd)', () => {
|
|
26
|
+
// freebsd is intentionally not in the supported allowlist — caller
|
|
27
|
+
// skips per-platform resolution and falls through to JS.
|
|
28
|
+
expect(getPlatformPackageName('freebsd', 'x64', null)).toBeNull()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('returns null for unsupported arch on a supported platform (linux ia32)', () => {
|
|
32
|
+
// ia32 is not in the matrix — released-native.yml builds x64 + arm64 only.
|
|
33
|
+
expect(getPlatformPackageName('linux', 'ia32', 'gnu')).toBeNull()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('returns null for darwin arm32 (not a real combo)', () => {
|
|
37
|
+
expect(getPlatformPackageName('darwin', 'arm', null)).toBeNull()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('returns null for win32 arm64 (not yet shipped)', () => {
|
|
41
|
+
// Phase 4 matrix doesn't include Windows ARM64. When it's added,
|
|
42
|
+
// bump both .github/workflows/release-native.yml AND the supported
|
|
43
|
+
// map in load-native.ts together.
|
|
44
|
+
expect(getPlatformPackageName('win32', 'arm64', 'msvc')).toBeNull()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('handles linux musl distinct from glibc', () => {
|
|
48
|
+
// The libc dimension is real — Alpine's musl binary is NOT
|
|
49
|
+
// ABI-compatible with Debian's glibc binary. Per-platform packages
|
|
50
|
+
// must differentiate.
|
|
51
|
+
expect(getPlatformPackageName('linux', 'x64', 'musl')).toBe('@pyreon/compiler-linux-x64-musl')
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -122,6 +122,25 @@ describeNative('Native vs JS equivalence — template emission', () => {
|
|
|
122
122
|
test('void element', () => compare('<div><br /><span>text</span></div>'))
|
|
123
123
|
test('ref in template (object)', () => compare('<div ref={myRef}><span /></div>'))
|
|
124
124
|
test('ref in template (arrow)', () => compare('<div ref={(el) => { myEl = el }}><span /></div>'))
|
|
125
|
+
|
|
126
|
+
// Regression: a child element with a block-arrow ref AND adjacent
|
|
127
|
+
// reactive props used to emit `const __e0 = __root.children[N]`
|
|
128
|
+
// followed by `((el) => { ... })(__e0)` with NO `;` between, so JS's
|
|
129
|
+
// ASI merged them into one expression `const __e0 = X((el) => ...)(__e0)`
|
|
130
|
+
// (calling X as fn, self-referencing __e0). Both backends now append
|
|
131
|
+
// `;` to every bind line. This test asserts both emit the SAME `;`-
|
|
132
|
+
// terminated output and the chained-call shape never appears.
|
|
133
|
+
test('block-arrow ref on child element with adjacent reactive prop', () => {
|
|
134
|
+
const input = '<div><span ref={(el) => { x = el }} data-state={cls()} /></div>'
|
|
135
|
+
compare(input)
|
|
136
|
+
// Tighter assertion: neither backend may emit the silent-merge shape.
|
|
137
|
+
const js = transformJSX_JS(input, 'test.tsx')
|
|
138
|
+
expect(js.code).not.toMatch(/children\[0\]\(\(/)
|
|
139
|
+
expect(js.code).toMatch(/const __e0 = __root\.children\[0\];/)
|
|
140
|
+
const rs = nativeTransform!(input, 'test.tsx', false, null)
|
|
141
|
+
expect(rs.code).not.toMatch(/children\[0\]\(\(/)
|
|
142
|
+
expect(rs.code).toMatch(/const __e0 = __root\.children\[0\];/)
|
|
143
|
+
})
|
|
125
144
|
test('non-delegated event', () => compare('<div onMouseEnter={handler}><span /></div>'))
|
|
126
145
|
test('style object in template', () => compare('<div style={{ overflow: "hidden" }}>text</div>'))
|
|
127
146
|
test('style string in template', () => compare('<div style="color: red">text</div>'))
|
|
@@ -652,3 +671,61 @@ describeNative('Native vs JS equivalence — knownSignals cross-module', () => {
|
|
|
652
671
|
['theme'],
|
|
653
672
|
))
|
|
654
673
|
})
|
|
674
|
+
|
|
675
|
+
// PR #352 added a `DOM_PROPS` set so `<input value={x()} />` inside a
|
|
676
|
+
// template-emitting context compiles to `el.value = x()` (property
|
|
677
|
+
// assignment) instead of `el.setAttribute("value", x())` (content
|
|
678
|
+
// attribute). The two diverge for IDL properties whose live state
|
|
679
|
+
// differs from the content attribute (`value`, `checked`, etc.). The
|
|
680
|
+
// Rust native backend reimplements this list separately. A typo in
|
|
681
|
+
// either side's list would silently produce wrong output for one
|
|
682
|
+
// DOM_PROP without breaking any other test. This block enumerates
|
|
683
|
+
// every DOM_PROP under template context (the only context where
|
|
684
|
+
// DOM_PROPS actually fires — root-level standalone JSX uses the
|
|
685
|
+
// `h()` path, not `_tpl() + _bind()`) and asserts JS↔Rust agreement,
|
|
686
|
+
// so a drift between the two lists fails one specific test.
|
|
687
|
+
//
|
|
688
|
+
// Reference: packages/core/compiler/src/jsx.ts:1389 — DOM_PROPS Set.
|
|
689
|
+
describeNative('Native vs JS equivalence — DOM properties', () => {
|
|
690
|
+
const DOM_PROPS = [
|
|
691
|
+
'value',
|
|
692
|
+
'checked',
|
|
693
|
+
'selected',
|
|
694
|
+
'disabled',
|
|
695
|
+
'multiple',
|
|
696
|
+
'readOnly',
|
|
697
|
+
'indeterminate',
|
|
698
|
+
] as const
|
|
699
|
+
|
|
700
|
+
for (const prop of DOM_PROPS) {
|
|
701
|
+
test(`DOM_PROP in template: <div><input ${prop}={x()} /></div> (reactive)`, () => {
|
|
702
|
+
compare(`<div><input ${prop}={x()} /></div>`)
|
|
703
|
+
})
|
|
704
|
+
|
|
705
|
+
test(`DOM_PROP in template: <div><input ${prop}={() => x()} /></div> (accessor)`, () => {
|
|
706
|
+
compare(`<div><input ${prop}={() => x()} /></div>`)
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
test(`DOM_PROP in template: <div><input ${prop}={true} /></div> (literal)`, () => {
|
|
710
|
+
compare(`<div><input ${prop}={true} /></div>`)
|
|
711
|
+
})
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
test('regression: all DOM_PROPS together in one template', () => {
|
|
715
|
+
// Sentinel — if a future PR adds a new DOM property to either
|
|
716
|
+
// backend without adding it to the other, the loop above won't
|
|
717
|
+
// notice unless that prop is in the test list. This single test
|
|
718
|
+
// compiles JSX with ALL known DOM_PROPS together and verifies
|
|
719
|
+
// both backends agree on the combined output.
|
|
720
|
+
const allProps = DOM_PROPS.map((p) => `${p}={x()}`).join(' ')
|
|
721
|
+
compare(`<div><input ${allProps} /></div>`)
|
|
722
|
+
})
|
|
723
|
+
|
|
724
|
+
test('non-DOM-prop control: title in template uses setAttribute, not assignment', () => {
|
|
725
|
+
// Negative control — `title` is NOT a DOM_PROP, so it should
|
|
726
|
+
// compile through setAttribute. If this test starts failing,
|
|
727
|
+
// someone added `title` to DOM_PROPS — verify intent before
|
|
728
|
+
// updating.
|
|
729
|
+
compare('<div><input title={x()} /></div>')
|
|
730
|
+
})
|
|
731
|
+
})
|