@fictjs/runtime 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/advanced.cjs +9 -9
  2. package/dist/advanced.js +4 -4
  3. package/dist/{chunk-CBRGOLTR.cjs → chunk-5FVWBK4M.cjs} +40 -40
  4. package/dist/{chunk-CBRGOLTR.cjs.map → chunk-5FVWBK4M.cjs.map} +1 -1
  5. package/dist/{chunk-BADX4WTQ.cjs → chunk-6DNYVH5U.cjs} +17 -17
  6. package/dist/{chunk-BADX4WTQ.cjs.map → chunk-6DNYVH5U.cjs.map} +1 -1
  7. package/dist/{chunk-MAHWGB55.js → chunk-CFAWL76V.js} +5 -5
  8. package/dist/{chunk-MAHWGB55.js.map → chunk-CFAWL76V.js.map} +1 -1
  9. package/dist/{chunk-WJMZ7X46.cjs → chunk-ECKYFH5Q.cjs} +5 -5
  10. package/dist/{chunk-WJMZ7X46.cjs.map → chunk-ECKYFH5Q.cjs.map} +1 -1
  11. package/dist/{chunk-ZJZ6LMDN.js → chunk-F5SDRX4J.js} +2 -2
  12. package/dist/{chunk-ZJZ6LMDN.js.map → chunk-F5SDRX4J.js.map} +1 -1
  13. package/dist/{chunk-4P4DYWLQ.js → chunk-IIWHTV23.js} +3 -3
  14. package/dist/{chunk-AR2T7JEX.cjs → chunk-INYTG4NG.cjs} +174 -174
  15. package/dist/{chunk-AR2T7JEX.cjs.map → chunk-INYTG4NG.cjs.map} +1 -1
  16. package/dist/{chunk-RK2WSQYL.js → chunk-M42N54LG.js} +3 -3
  17. package/dist/{chunk-ZWQLXWSV.js → chunk-UQTWIV3S.js} +3 -3
  18. package/dist/{chunk-ECNK25S4.cjs → chunk-WY4LI5PB.cjs} +8 -8
  19. package/dist/{chunk-ECNK25S4.cjs.map → chunk-WY4LI5PB.cjs.map} +1 -1
  20. package/dist/index.cjs +42 -42
  21. package/dist/index.d.cts +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.dev.js.map +1 -1
  24. package/dist/index.js +3 -3
  25. package/dist/internal-list.cjs +4 -4
  26. package/dist/internal-list.js +3 -3
  27. package/dist/internal.cjs +5 -5
  28. package/dist/internal.d.cts +2 -2
  29. package/dist/internal.d.ts +2 -2
  30. package/dist/internal.js +4 -4
  31. package/dist/jsx-dev-runtime.cjs.map +1 -1
  32. package/dist/jsx-dev-runtime.d.cts +46 -0
  33. package/dist/jsx-dev-runtime.d.ts +46 -0
  34. package/dist/jsx-dev-runtime.js.map +1 -1
  35. package/dist/jsx-runtime.cjs.map +1 -1
  36. package/dist/jsx-runtime.d.cts +46 -0
  37. package/dist/jsx-runtime.d.ts +46 -0
  38. package/dist/jsx-runtime.js.map +1 -1
  39. package/dist/loader.cjs +143 -26
  40. package/dist/loader.cjs.map +1 -1
  41. package/dist/loader.d.cts +1 -1
  42. package/dist/loader.d.ts +1 -1
  43. package/dist/loader.js +122 -5
  44. package/dist/loader.js.map +1 -1
  45. package/dist/{props-DabFQwLR.d.ts → props-C04ScJgm.d.ts} +46 -0
  46. package/dist/{props-tImUZAty.d.cts → props-CdmuXCiu.d.cts} +46 -0
  47. package/dist/{resume-C5IKAIdh.d.ts → resume-C166aAVg.d.ts} +2 -2
  48. package/dist/{resume-DPZxmA95.d.cts → resume-C20cRVj9.d.cts} +2 -2
  49. package/package.json +1 -1
  50. package/src/jsx.ts +46 -0
  51. package/src/loader.ts +153 -4
  52. package/src/resume.ts +5 -5
  53. /package/dist/{chunk-4P4DYWLQ.js.map → chunk-IIWHTV23.js.map} +0 -0
  54. /package/dist/{chunk-RK2WSQYL.js.map → chunk-M42N54LG.js.map} +0 -0
  55. /package/dist/{chunk-ZWQLXWSV.js.map → chunk-UQTWIV3S.js.map} +0 -0
package/src/loader.ts CHANGED
@@ -36,6 +36,115 @@ function resolveModuleUrl(url: string): string {
36
36
  return url
37
37
  }
38
38
 
39
+ function resolveAbsoluteModuleUrl(url: string, ownerDocument?: Document): string {
40
+ const baseUrl =
41
+ ownerDocument?.baseURI ?? (typeof document !== 'undefined' ? document.baseURI : undefined)
42
+ if (!baseUrl) return url
43
+
44
+ try {
45
+ return new URL(url, baseUrl).href
46
+ } catch {
47
+ return url
48
+ }
49
+ }
50
+
51
+ function normalizeImportUrl(url: string): string {
52
+ if (!url.startsWith('data:')) {
53
+ return url
54
+ }
55
+
56
+ const dataSeparatorIndex = url.indexOf(',')
57
+ if (dataSeparatorIndex === -1) {
58
+ return url
59
+ }
60
+
61
+ const metadata = url.slice(0, dataSeparatorIndex)
62
+ const payload = url.slice(dataSeparatorIndex + 1)
63
+ if (metadata.includes(';base64')) {
64
+ return url
65
+ }
66
+
67
+ try {
68
+ return `${metadata},${encodeURIComponent(decodeURIComponent(payload))}`
69
+ } catch {
70
+ return `${metadata},${encodeURIComponent(payload)}`
71
+ }
72
+ }
73
+
74
+ interface PreservedControlState {
75
+ value?: string
76
+ checked?: boolean
77
+ selectedIndex?: number
78
+ selectedValues?: string[]
79
+ }
80
+
81
+ function captureControlState(node: Element, event: Event): PreservedControlState | null {
82
+ if (event.type !== 'input' && event.type !== 'change') return null
83
+
84
+ if (node instanceof HTMLInputElement) {
85
+ if (node.type === 'file') return null
86
+ if (node.type === 'checkbox' || node.type === 'radio') {
87
+ return { checked: node.checked }
88
+ }
89
+ return { value: node.value }
90
+ }
91
+
92
+ if (node instanceof HTMLTextAreaElement) {
93
+ return { value: node.value }
94
+ }
95
+
96
+ if (node instanceof HTMLSelectElement) {
97
+ return node.multiple
98
+ ? {
99
+ selectedValues: Array.from(node.selectedOptions).map(option => option.value),
100
+ }
101
+ : {
102
+ value: node.value,
103
+ selectedIndex: node.selectedIndex,
104
+ }
105
+ }
106
+
107
+ return null
108
+ }
109
+
110
+ function restoreControlState(node: Element, state: PreservedControlState | null): void {
111
+ if (!state) return
112
+
113
+ if (node instanceof HTMLInputElement) {
114
+ if (typeof state.checked === 'boolean') {
115
+ node.checked = state.checked
116
+ }
117
+ if (typeof state.value === 'string' && node.type !== 'file') {
118
+ node.value = state.value
119
+ }
120
+ return
121
+ }
122
+
123
+ if (node instanceof HTMLTextAreaElement) {
124
+ if (typeof state.value === 'string') {
125
+ node.value = state.value
126
+ }
127
+ return
128
+ }
129
+
130
+ if (node instanceof HTMLSelectElement) {
131
+ if (Array.isArray(state.selectedValues) && node.multiple) {
132
+ const selected = new Set(state.selectedValues)
133
+ for (const option of Array.from(node.options)) {
134
+ option.selected = selected.has(option.value)
135
+ }
136
+ return
137
+ }
138
+
139
+ if (typeof state.selectedIndex === 'number') {
140
+ node.selectedIndex = state.selectedIndex
141
+ }
142
+ if (typeof state.value === 'string') {
143
+ node.value = state.value
144
+ }
145
+ }
146
+ }
147
+
39
148
  // ============================================================================
40
149
  // Types
41
150
  // ============================================================================
@@ -557,29 +666,69 @@ async function handleResumableEventAsync(event: Event): Promise<void> {
557
666
  }
558
667
  }
559
668
 
669
+ const preservedControlState = captureControlState(node, event)
670
+ let resumedDuringEvent = false
671
+
560
672
  // Resume FIRST to set up reactive bindings BEFORE the handler runs
561
673
  if (!hydratedScopes.has(scopeId)) {
562
674
  const resumeQrl = host.getAttribute('data-fict-h')
563
675
  if (resumeQrl) {
564
676
  const { url: resumeUrl, exportName: resumeExport } = parseQrl(resumeQrl)
565
677
  const resolvedResumeUrl = resolveModuleUrl(resumeUrl)
678
+ const resolvedAbsoluteResumeUrl = resolveAbsoluteModuleUrl(
679
+ resolvedResumeUrl,
680
+ host.ownerDocument ?? undefined,
681
+ )
682
+ const resolvedResumeQrl = `${resolvedResumeUrl}#${resumeExport}`
683
+ const resolvedAbsoluteResumeQrl = `${resolvedAbsoluteResumeUrl}#${resumeExport}`
684
+ const normalizedResumeImportUrl = normalizeImportUrl(resolvedResumeUrl)
566
685
  // Load the module to ensure resume functions are registered
567
- await import(/* @vite-ignore */ resolvedResumeUrl)
686
+ await import(/* @vite-ignore */ normalizedResumeImportUrl)
568
687
  // Get resume function from registry (not module exports)
569
- const resumeFn = __fictGetResume(resumeExport)
688
+ const resumeFn =
689
+ __fictGetResume(resumeQrl) ??
690
+ __fictGetResume(resolvedResumeQrl) ??
691
+ __fictGetResume(resolvedAbsoluteResumeQrl) ??
692
+ __fictGetResume(resumeExport)
570
693
  if (typeof resumeFn === 'function') {
571
694
  await (resumeFn as (scopeId: string, host: Element) => unknown)(scopeId, host)
572
695
  hydratedScopes.add(scopeId)
696
+ resumedDuringEvent = true
573
697
  }
574
698
  }
575
699
  }
576
700
 
701
+ if (resumedDuringEvent) {
702
+ restoreControlState(node, preservedControlState)
703
+ }
704
+
577
705
  // THEN run the handler - now signal updates will trigger DOM updates
578
706
  const resolvedUrl = resolveModuleUrl(url)
579
- const mod = await import(/* @vite-ignore */ resolvedUrl)
707
+ const normalizedImportUrl = normalizeImportUrl(resolvedUrl)
708
+ const mod = await import(/* @vite-ignore */ normalizedImportUrl)
580
709
  const handler = (mod as Record<string, unknown>)[exportName]
581
710
  if (typeof handler === 'function') {
582
- await (handler as (scopeId: string, ev: Event, el: Element) => unknown)(scopeId, event, node)
711
+ const originalCurrentTarget = event.currentTarget
712
+ Object.defineProperty(event, 'currentTarget', {
713
+ configurable: true,
714
+ get() {
715
+ return node
716
+ },
717
+ })
718
+ try {
719
+ await (handler as (scopeId: string, ev: Event, el: Element) => unknown)(
720
+ scopeId,
721
+ event,
722
+ node,
723
+ )
724
+ } finally {
725
+ Object.defineProperty(event, 'currentTarget', {
726
+ configurable: true,
727
+ get() {
728
+ return originalCurrentTarget
729
+ },
730
+ })
731
+ }
583
732
  }
584
733
 
585
734
  return
package/src/resume.ts CHANGED
@@ -294,23 +294,23 @@ export function __fictQrl(moduleId: string, exportName: string): string {
294
294
  return `${moduleId}#${exportName}`
295
295
  }
296
296
 
297
- // Registry for resume functions to prevent tree-shaking
297
+ // Registry for resume functions to prevent tree-shaking.
298
298
  const resumeFunctionRegistry = new Map<string, (...args: unknown[]) => unknown>()
299
299
 
300
300
  /**
301
301
  * Register a resume function to prevent it from being tree-shaken.
302
302
  * This is called at module load time by compiled component code.
303
303
  */
304
- export function __fictRegisterResume(name: string, fn: (...args: unknown[]) => unknown): void {
305
- resumeFunctionRegistry.set(name, fn)
304
+ export function __fictRegisterResume(key: string, fn: (...args: unknown[]) => unknown): void {
305
+ resumeFunctionRegistry.set(key, fn)
306
306
  }
307
307
 
308
308
  /**
309
309
  * Get a registered resume function by name.
310
310
  * Used by the loader to find resume functions.
311
311
  */
312
- export function __fictGetResume(name: string): ((...args: unknown[]) => unknown) | undefined {
313
- return resumeFunctionRegistry.get(name)
312
+ export function __fictGetResume(key: string): ((...args: unknown[]) => unknown) | undefined {
313
+ return resumeFunctionRegistry.get(key)
314
314
  }
315
315
 
316
316
  function serializeSlots(ctx: HookContext): SlotSnapshot[] {