pywire 0.1.0__py3-none-any.whl → 0.1.1__py3-none-any.whl

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 (101) hide show
  1. {pywire-0.1.0.dist-info → pywire-0.1.1.dist-info}/METADATA +23 -1
  2. pywire-0.1.1.dist-info/RECORD +9 -0
  3. pywire/__init__.py +0 -2
  4. pywire/cli/__init__.py +0 -1
  5. pywire/cli/generators.py +0 -48
  6. pywire/cli/main.py +0 -309
  7. pywire/cli/tui.py +0 -563
  8. pywire/cli/validate.py +0 -26
  9. pywire/client/.prettierignore +0 -8
  10. pywire/client/.prettierrc +0 -7
  11. pywire/client/build.mjs +0 -73
  12. pywire/client/eslint.config.js +0 -46
  13. pywire/client/package.json +0 -39
  14. pywire/client/pnpm-lock.yaml +0 -2971
  15. pywire/client/src/core/app.ts +0 -263
  16. pywire/client/src/core/dom-updater.test.ts +0 -78
  17. pywire/client/src/core/dom-updater.ts +0 -321
  18. pywire/client/src/core/index.ts +0 -5
  19. pywire/client/src/core/transport-manager.test.ts +0 -179
  20. pywire/client/src/core/transport-manager.ts +0 -159
  21. pywire/client/src/core/transports/base.ts +0 -122
  22. pywire/client/src/core/transports/http.ts +0 -142
  23. pywire/client/src/core/transports/index.ts +0 -13
  24. pywire/client/src/core/transports/websocket.ts +0 -97
  25. pywire/client/src/core/transports/webtransport.ts +0 -149
  26. pywire/client/src/dev/dev-app.ts +0 -93
  27. pywire/client/src/dev/error-trace.test.ts +0 -97
  28. pywire/client/src/dev/error-trace.ts +0 -76
  29. pywire/client/src/dev/index.ts +0 -4
  30. pywire/client/src/dev/status-overlay.ts +0 -63
  31. pywire/client/src/events/handler.test.ts +0 -318
  32. pywire/client/src/events/handler.ts +0 -454
  33. pywire/client/src/pywire.core.ts +0 -22
  34. pywire/client/src/pywire.dev.ts +0 -27
  35. pywire/client/tsconfig.json +0 -17
  36. pywire/client/vitest.config.ts +0 -15
  37. pywire/compiler/__init__.py +0 -6
  38. pywire/compiler/ast_nodes.py +0 -304
  39. pywire/compiler/attributes/__init__.py +0 -6
  40. pywire/compiler/attributes/base.py +0 -24
  41. pywire/compiler/attributes/conditional.py +0 -37
  42. pywire/compiler/attributes/events.py +0 -55
  43. pywire/compiler/attributes/form.py +0 -37
  44. pywire/compiler/attributes/loop.py +0 -75
  45. pywire/compiler/attributes/reactive.py +0 -34
  46. pywire/compiler/build.py +0 -28
  47. pywire/compiler/build_artifacts.py +0 -342
  48. pywire/compiler/codegen/__init__.py +0 -5
  49. pywire/compiler/codegen/attributes/__init__.py +0 -6
  50. pywire/compiler/codegen/attributes/base.py +0 -19
  51. pywire/compiler/codegen/attributes/events.py +0 -35
  52. pywire/compiler/codegen/directives/__init__.py +0 -6
  53. pywire/compiler/codegen/directives/base.py +0 -16
  54. pywire/compiler/codegen/directives/path.py +0 -53
  55. pywire/compiler/codegen/generator.py +0 -2341
  56. pywire/compiler/codegen/template.py +0 -2178
  57. pywire/compiler/directives/__init__.py +0 -7
  58. pywire/compiler/directives/base.py +0 -20
  59. pywire/compiler/directives/component.py +0 -33
  60. pywire/compiler/directives/context.py +0 -93
  61. pywire/compiler/directives/layout.py +0 -49
  62. pywire/compiler/directives/no_spa.py +0 -24
  63. pywire/compiler/directives/path.py +0 -71
  64. pywire/compiler/directives/props.py +0 -88
  65. pywire/compiler/exceptions.py +0 -19
  66. pywire/compiler/interpolation/__init__.py +0 -6
  67. pywire/compiler/interpolation/base.py +0 -28
  68. pywire/compiler/interpolation/jinja.py +0 -272
  69. pywire/compiler/parser.py +0 -750
  70. pywire/compiler/paths.py +0 -29
  71. pywire/compiler/preprocessor.py +0 -43
  72. pywire/core/wire.py +0 -119
  73. pywire/py.typed +0 -0
  74. pywire/runtime/__init__.py +0 -7
  75. pywire/runtime/aioquic_server.py +0 -194
  76. pywire/runtime/app.py +0 -889
  77. pywire/runtime/compile_error_page.py +0 -195
  78. pywire/runtime/debug.py +0 -203
  79. pywire/runtime/dev_server.py +0 -434
  80. pywire/runtime/dev_server.py.broken +0 -268
  81. pywire/runtime/error_page.py +0 -64
  82. pywire/runtime/error_renderer.py +0 -23
  83. pywire/runtime/escape.py +0 -23
  84. pywire/runtime/files.py +0 -40
  85. pywire/runtime/helpers.py +0 -97
  86. pywire/runtime/http_transport.py +0 -253
  87. pywire/runtime/loader.py +0 -272
  88. pywire/runtime/logging.py +0 -72
  89. pywire/runtime/page.py +0 -384
  90. pywire/runtime/pydantic_integration.py +0 -52
  91. pywire/runtime/router.py +0 -229
  92. pywire/runtime/server.py +0 -25
  93. pywire/runtime/style_collector.py +0 -31
  94. pywire/runtime/upload_manager.py +0 -76
  95. pywire/runtime/validation.py +0 -449
  96. pywire/runtime/websocket.py +0 -665
  97. pywire/runtime/webtransport_handler.py +0 -195
  98. pywire-0.1.0.dist-info/RECORD +0 -104
  99. {pywire-0.1.0.dist-info → pywire-0.1.1.dist-info}/WHEEL +0 -0
  100. {pywire-0.1.0.dist-info → pywire-0.1.1.dist-info}/entry_points.txt +0 -0
  101. {pywire-0.1.0.dist-info → pywire-0.1.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,263 +0,0 @@
1
- import { TransportManager, TransportConfig } from './transport-manager'
2
- import { DOMUpdater } from './dom-updater'
3
- import { ServerMessage, ClientMessage, EventData, RelocateMessage } from './transports'
4
- import { UnifiedEventHandler } from '../events/handler'
5
-
6
- export interface PyWireConfig extends TransportConfig {
7
- /** Auto-initialize on DOMContentLoaded */
8
- autoInit?: boolean
9
- /** Enable verbose debug logging */
10
- debug?: boolean
11
- }
12
-
13
- const DEFAULT_CONFIG: PyWireConfig = {
14
- autoInit: true,
15
- enableWebTransport: true,
16
- enableWebSocket: true,
17
- enableHTTP: true,
18
- debug: false,
19
- }
20
-
21
- /**
22
- * Core PyWire Application class.
23
- * Provides transport, DOM updates, SPA navigation, and event handling.
24
- * Dev-only features (status overlay, error traces) are in the dev bundle.
25
- */
26
- export class PyWireApp {
27
- protected transport: TransportManager
28
- protected updater: DOMUpdater
29
- protected eventHandler: UnifiedEventHandler
30
- protected initialized = false
31
- protected config: PyWireConfig
32
- protected siblingPaths: string[] = []
33
- protected pathRegexes: RegExp[] = []
34
- protected pjaxEnabled = false
35
- protected isConnected = false
36
-
37
- constructor(config: Partial<PyWireConfig> = {}) {
38
- this.config = { ...DEFAULT_CONFIG, ...config }
39
- this.transport = new TransportManager(this.config)
40
- this.updater = new DOMUpdater(this.config.debug)
41
- this.eventHandler = new UnifiedEventHandler(this)
42
- }
43
-
44
- getConfig(): PyWireConfig {
45
- return this.config
46
- }
47
-
48
- /**
49
- * Initialize the PyWire application.
50
- */
51
- async init(): Promise<void> {
52
- if (this.initialized) return
53
- this.initialized = true
54
-
55
- // Setup message handling
56
- this.transport.onMessage((msg) => this.handleMessage(msg))
57
- this.transport.onStatusChange((connected) => this.handleStatusChange(connected))
58
-
59
- // Connect transport with fallback
60
- try {
61
- await this.transport.connect()
62
- } catch (e) {
63
- console.error('PyWire: Failed to connect:', e)
64
- }
65
-
66
- // Load SPA metadata and setup navigation
67
- this.loadSPAMetadata()
68
- this.setupSPANavigation()
69
-
70
- // Setup event interception via UnifiedEventHandler
71
- this.eventHandler.init()
72
-
73
- console.log(
74
- `PyWire: Initialized (transport: ${this.transport.getActiveTransport()}, spa_paths: ${this.siblingPaths.length}, pjax: ${this.pjaxEnabled})`
75
- )
76
- }
77
-
78
- /**
79
- * Handle connection status changes. Override in dev bundle for UI.
80
- */
81
- protected handleStatusChange(connected: boolean): void {
82
- this.isConnected = connected
83
- // Base implementation: no UI, just track state
84
- }
85
-
86
- /**
87
- * Load SPA navigation metadata from injected script tag.
88
- */
89
- protected loadSPAMetadata(): void {
90
- const metaScript = document.getElementById('_pywire_spa_meta')
91
- if (metaScript) {
92
- try {
93
- const meta = JSON.parse(metaScript.textContent || '{}')
94
- this.siblingPaths = meta.sibling_paths || []
95
- this.pjaxEnabled = !!meta.enable_pjax
96
- if (meta.debug !== undefined) {
97
- this.config.debug = !!meta.debug
98
- }
99
- // Convert path patterns to regexes for matching
100
- this.pathRegexes = this.siblingPaths.map((p) => this.patternToRegex(p))
101
- } catch (e) {
102
- console.warn('PyWire: Failed to parse SPA metadata', e)
103
- }
104
- }
105
- }
106
-
107
- /**
108
- * Convert route pattern like '/a/:id' to regex.
109
- */
110
- protected patternToRegex(pattern: string): RegExp {
111
- // Escape special regex chars except for our placeholders
112
- let regex = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
113
- // Replace :param:type or :param with capture groups
114
- regex = regex.replace(/:(\\w+)(:\\w+)?/g, '([^/]+)')
115
- // Replace {param:type} or {param} with capture groups
116
- regex = regex.replace(/\\{(\\w+)(:\\w+)?\\}/g, '([^/]+)')
117
- return new RegExp(`^${regex}$`)
118
- }
119
-
120
- /**
121
- * Check if a path matches any sibling path pattern.
122
- */
123
- protected isSiblingPath(path: string): boolean {
124
- return this.pathRegexes.some((regex) => regex.test(path))
125
- }
126
-
127
- /**
128
- * Setup SPA navigation for sibling paths.
129
- */
130
- protected setupSPANavigation(): void {
131
- // Handle browser back/forward
132
- window.addEventListener('popstate', () => {
133
- this.sendRelocate(window.location.pathname + window.location.search)
134
- })
135
-
136
- if (this.siblingPaths.length === 0 && !this.pjaxEnabled) return
137
-
138
- // Intercept link clicks
139
- document.addEventListener('click', (e) => {
140
- const link = (e.target as Element).closest('a[href]') as HTMLAnchorElement | null
141
- if (!link) return
142
-
143
- // Only intercept same-origin links
144
- if (link.origin !== window.location.origin) return
145
-
146
- // Ignore special links
147
- if (link.hasAttribute('download') || link.target === '_blank') return
148
-
149
- // Check if matches criteria
150
- let shouldIntercept = false
151
-
152
- if (this.pjaxEnabled) {
153
- shouldIntercept = true
154
- } else if (this.isSiblingPath(link.pathname)) {
155
- shouldIntercept = true
156
- }
157
-
158
- if (shouldIntercept) {
159
- e.preventDefault()
160
- this.navigateTo(link.pathname + link.search)
161
- }
162
- })
163
- }
164
-
165
- /**
166
- * Navigate to a path using SPA navigation.
167
- */
168
- navigateTo(path: string): void {
169
- if (!this.isConnected) {
170
- console.warn('PyWire: Navigation blocked - Offline')
171
- return
172
- }
173
-
174
- history.pushState({}, '', path)
175
- this.sendRelocate(path)
176
- }
177
-
178
- /**
179
- * Send relocate message to server.
180
- */
181
- protected sendRelocate(path: string): void {
182
- const message: RelocateMessage = {
183
- type: 'relocate',
184
- path,
185
- }
186
- this.transport.send(message)
187
- }
188
-
189
- /**
190
- * Send an event to the server.
191
- */
192
- sendEvent(handler: string, data: EventData): void {
193
- const message: ClientMessage = {
194
- type: 'event',
195
- handler,
196
- path: window.location.pathname + window.location.search,
197
- data,
198
- }
199
- this.transport.send(message)
200
- }
201
-
202
- /**
203
- * Handle incoming server message. Override in dev bundle for error_trace.
204
- */
205
- protected async handleMessage(msg: ServerMessage): Promise<void> {
206
- switch (msg.type) {
207
- case 'update':
208
- if (msg.regions && msg.regions.length > 0) {
209
- msg.regions.forEach((update) => {
210
- this.updater.updateRegion(update.region, update.html)
211
- })
212
- } else if (msg.html) {
213
- this.updater.update(msg.html)
214
- }
215
- break
216
-
217
- case 'reload':
218
- console.log('PyWire: Reloading...')
219
- window.location.reload()
220
- break
221
-
222
- case 'error':
223
- console.error('PyWire: Server error:', msg.error)
224
- break
225
-
226
- case 'error_trace':
227
- // In core bundle, just log the error (no source loading)
228
- console.error('PyWire: Error:', msg.error)
229
- break
230
-
231
- case 'console':
232
- if (msg.lines && msg.lines.length > 0) {
233
- const prefix = 'PyWire Server:'
234
- const joined = msg.lines.join('\n')
235
- if (msg.level === 'error') {
236
- console.error(prefix, joined)
237
- } else if (msg.level === 'warn') {
238
- console.warn(prefix, joined)
239
- } else {
240
- console.log(prefix, joined)
241
- }
242
- }
243
- break
244
-
245
- default:
246
- console.warn('PyWire: Unknown message type', msg)
247
- }
248
- }
249
-
250
- /**
251
- * Get the current transport name.
252
- */
253
- getTransport(): string | null {
254
- return this.transport.getActiveTransport()
255
- }
256
-
257
- /**
258
- * Disconnect from the server.
259
- */
260
- disconnect(): void {
261
- this.transport.disconnect()
262
- }
263
- }
@@ -1,78 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest'
2
- import { DOMUpdater } from './dom-updater'
3
- import morphdom from 'morphdom'
4
-
5
- vi.mock('morphdom', () => ({
6
- default: vi.fn((_from, _to, _options) => {
7
- // Basic simulation of morphdom: just replace content if no options,
8
- // or call hooks if provided (we only care about onBeforeElUpdated)
9
- return
10
- }),
11
- }))
12
-
13
- describe('DOMUpdater', () => {
14
- let updater: DOMUpdater
15
-
16
- beforeEach(() => {
17
- vi.clearAllMocks()
18
- updater = new DOMUpdater()
19
- document.documentElement.innerHTML = '<body><div id="app"></div></body>'
20
- })
21
-
22
- it('should call morphdom with custom options', () => {
23
- updater.update('<html><body><div id="app">New</div></body></html>')
24
- expect(morphdom).toHaveBeenCalledWith(
25
- expect.any(Node),
26
- expect.stringContaining('New'),
27
- expect.objectContaining({
28
- onBeforeElUpdated: expect.any(Function),
29
- })
30
- )
31
- })
32
-
33
- it('should preserve input value if focused and similar', () => {
34
- const morphdomMock = vi.mocked(morphdom)
35
- updater.update('<html><body><input id="test" value="server"></body></html>')
36
-
37
- // Get the onBeforeElUpdated hook
38
- const options = morphdomMock.mock.calls[0][2]
39
- const onBeforeElUpdated = options?.onBeforeElUpdated
40
-
41
- if (!onBeforeElUpdated) throw new Error('Hook not found')
42
-
43
- const fromEl = document.createElement('input')
44
- fromEl.value = 'server-ahead'
45
- vi.spyOn(document, 'activeElement', 'get').mockReturnValue(fromEl)
46
-
47
- const toEl = document.createElement('input')
48
- toEl.setAttribute('value', 'server')
49
-
50
- const result = onBeforeElUpdated(fromEl, toEl)
51
-
52
- expect(result).toBe(true)
53
- expect(toEl.value).toBe('server-ahead')
54
- })
55
-
56
- it('should NOT preserve input value if completely different', () => {
57
- const morphdomMock = vi.mocked(morphdom)
58
- updater.update('<html><body><input id="test" value="server"></body></html>')
59
-
60
- const options = morphdomMock.mock.calls[0][2]
61
- const onBeforeElUpdated = options?.onBeforeElUpdated
62
-
63
- if (!onBeforeElUpdated) throw new Error('Hook not found')
64
-
65
- const fromEl = document.createElement('input')
66
- fromEl.value = 'user-typed-something-else'
67
- vi.spyOn(document, 'activeElement', 'get').mockReturnValue(fromEl)
68
-
69
- const toEl = document.createElement('input')
70
- toEl.setAttribute('value', 'server-new')
71
- toEl.value = 'server-new'
72
-
73
- onBeforeElUpdated(fromEl, toEl)
74
-
75
- // Should NOT have overwritten toEl.value with fromEl.value because they don't start with each other
76
- expect(toEl.value).toBe('server-new')
77
- })
78
- })
@@ -1,321 +0,0 @@
1
- /**
2
- * DOM Updater using morphdom for efficient DOM diffing.
3
- */
4
- import morphdom from 'morphdom'
5
-
6
- interface FocusState {
7
- /** CSS selector to find the element */
8
- selector: string
9
- /** Element ID if available */
10
- id: string | null
11
- tagName: string
12
- selectionStart: number | null
13
- selectionEnd: number | null
14
- scrollTop: number
15
- scrollLeft: number
16
- value: string
17
- }
18
-
19
- export class DOMUpdater {
20
- /**
21
- * Flag to indicate DOM is being updated.
22
- * Event handlers should check this to avoid triggering events during updates.
23
- */
24
- static isUpdating = false
25
-
26
- private debug: boolean
27
-
28
- constructor(debug: boolean = false) {
29
- this.debug = debug
30
- }
31
-
32
- /**
33
- * Generate a stable key for an element.
34
- * Used by morphdom to match elements between old and new DOM.
35
- */
36
- private getNodeKey(node: Node): string | undefined {
37
- if (!(node instanceof HTMLElement)) return undefined
38
-
39
- // 1. Use data-on-* handler as key FIRST (stable across renders)
40
- for (const attr of node.attributes) {
41
- if (attr.name.startsWith('data-on-')) {
42
- const key = `${node.tagName}-${attr.name}-${attr.value}`
43
- return key
44
- }
45
- }
46
-
47
- // 2. Use explicit ID (but skip client-generated pywire-uid-* IDs)
48
- if (node.id && !node.id.startsWith('pywire-uid-')) {
49
- return node.id
50
- }
51
-
52
- // 3. Use name attribute for form elements
53
- if (
54
- node instanceof HTMLInputElement ||
55
- node instanceof HTMLSelectElement ||
56
- node instanceof HTMLTextAreaElement
57
- ) {
58
- if (node.name) {
59
- return `${node.tagName}-name-${node.name}`
60
- }
61
- }
62
-
63
- // 4. For other elements, no key (morphdom will use position-based matching)
64
- return undefined
65
- }
66
-
67
- /**
68
- * Generate a selector to find an element
69
- */
70
- private getElementSelector(el: Element): string {
71
- if (el.id) return `#${el.id}`
72
-
73
- // Build a path-based selector
74
- const path: string[] = []
75
- let current: Element | null = el
76
-
77
- while (current && current !== document.body && path.length < 5) {
78
- let selector = current.tagName.toLowerCase()
79
-
80
- // Add distinguishing attributes
81
- if (current.id) {
82
- selector = `#${current.id}`
83
- path.unshift(selector)
84
- break // ID is unique enough
85
- }
86
-
87
- // Use name for form elements
88
- if (
89
- current instanceof HTMLInputElement ||
90
- current instanceof HTMLSelectElement ||
91
- current instanceof HTMLTextAreaElement
92
- ) {
93
- if (current.name) {
94
- selector += `[name="${current.name}"]`
95
- }
96
- }
97
-
98
- // Use data-on-* for event elements
99
- for (const attr of current.attributes) {
100
- if (attr.name.startsWith('data-on-')) {
101
- selector += `[${attr.name}="${attr.value}"]`
102
- break
103
- }
104
- }
105
-
106
- // Add nth-child for disambiguation
107
- if (current.parentElement) {
108
- const sibs = Array.from(current.parentElement.children)
109
- const sameTags = sibs.filter((s) => s.tagName === current!.tagName)
110
- if (sameTags.length > 1) {
111
- const idx = sameTags.indexOf(current) + 1
112
- selector += `:nth-of-type(${idx})`
113
- }
114
- }
115
-
116
- path.unshift(selector)
117
- current = current.parentElement
118
- }
119
-
120
- return path.join(' > ')
121
- }
122
-
123
- /**
124
- * Capture the current focus state before updating.
125
- */
126
- private captureFocusState(): FocusState | null {
127
- const active = document.activeElement
128
- if (!active || active === document.body || active === document.documentElement) return null
129
-
130
- const state: FocusState = {
131
- selector: this.getElementSelector(active),
132
- id: active.id || null,
133
- tagName: active.tagName,
134
- selectionStart: null,
135
- selectionEnd: null,
136
- scrollTop: 0,
137
- scrollLeft: 0,
138
- value: '',
139
- }
140
-
141
- if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement) {
142
- state.selectionStart = active.selectionStart
143
- state.selectionEnd = active.selectionEnd
144
- state.scrollTop = active.scrollTop
145
- state.scrollLeft = active.scrollLeft
146
- state.value = active.value
147
- }
148
-
149
- return state
150
- }
151
-
152
- /**
153
- * Restore focus state after updating.
154
- */
155
- private restoreFocusState(state: FocusState | null): void {
156
- if (!state) return
157
-
158
- // Try to find by ID first, then by selector
159
- let el: Element | null = null
160
- if (state.id) {
161
- el = document.getElementById(state.id)
162
- }
163
- if (!el && state.selector) {
164
- try {
165
- el = document.querySelector(state.selector)
166
- } catch {
167
- // Invalid selector, skip
168
- }
169
- }
170
-
171
- if (!el) return // Restore focus
172
- ;(el as HTMLElement).focus()
173
-
174
- // Restore selection/caret position
175
- if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
176
- // Restore value if it matches what we captured
177
- if (state.value && el.value !== state.value) {
178
- el.value = state.value
179
- }
180
-
181
- if (state.selectionStart !== null && state.selectionEnd !== null) {
182
- try {
183
- el.setSelectionRange(state.selectionStart, state.selectionEnd)
184
- } catch {
185
- // Some input types (date, number) don't support setSelectionRange
186
- }
187
- }
188
- el.scrollTop = state.scrollTop
189
- el.scrollLeft = state.scrollLeft
190
- }
191
- }
192
-
193
- private applyUpdate(target: Element, newHtml: string): void {
194
- // Set flag to suppress focus/blur events during update
195
- DOMUpdater.isUpdating = true
196
- if (this.debug) {
197
- console.log('[DOMUpdater] Starting update, isUpdating =', DOMUpdater.isUpdating)
198
- }
199
-
200
- try {
201
- // Capture focus before morphdom runs
202
- const focusState = this.captureFocusState()
203
-
204
- if (morphdom) {
205
- try {
206
- morphdom(target, newHtml, {
207
- // Custom key function for stable element matching
208
- getNodeKey: (node: Node) => this.getNodeKey(node),
209
-
210
- onBeforeElUpdated: (fromEl, toEl) => {
211
- // Transfer ALL relevant state from old element to new element
212
-
213
- // Input/Textarea: preserve value ONLY if they are broadly similar
214
- // (e.g. user is still typing or deleted a few chars).
215
- // If the server sends a completely different value, let it win.
216
- if (fromEl instanceof HTMLInputElement && toEl instanceof HTMLInputElement) {
217
- if (fromEl.type === 'checkbox' || fromEl.type === 'radio') {
218
- toEl.checked = fromEl.checked
219
- } else {
220
- const s = toEl.value || ''
221
- const c = fromEl.value || ''
222
- if (c.startsWith(s) || s.startsWith(c)) {
223
- toEl.value = c
224
- }
225
- }
226
- }
227
-
228
- if (fromEl instanceof HTMLTextAreaElement && toEl instanceof HTMLTextAreaElement) {
229
- const s = toEl.value || ''
230
- const c = fromEl.value || ''
231
- if (c.startsWith(s) || s.startsWith(c)) {
232
- toEl.value = c
233
- }
234
- }
235
-
236
- // Select: preserve selected option
237
- if (fromEl instanceof HTMLSelectElement && toEl instanceof HTMLSelectElement) {
238
- // Preserve by value (more robust than index)
239
- if (
240
- fromEl.value &&
241
- Array.from(toEl.options).some((o) => o.value === fromEl.value)
242
- ) {
243
- toEl.value = fromEl.value
244
- } else if (
245
- fromEl.selectedIndex >= 0 &&
246
- fromEl.selectedIndex < toEl.options.length
247
- ) {
248
- toEl.selectedIndex = fromEl.selectedIndex
249
- }
250
- }
251
-
252
- // Preserve client-generated IDs (vital for debouncers/throttlers that key off ID)
253
- if (fromEl.id && fromEl.id.startsWith('pywire-uid-') && !toEl.id) {
254
- toEl.id = fromEl.id
255
- }
256
-
257
- return true
258
- },
259
-
260
- onBeforeNodeDiscarded: () => true,
261
- })
262
- } catch (e) {
263
- console.error('Morphdom failed:', e)
264
- if (target === document.documentElement) {
265
- document.open()
266
- document.write(newHtml)
267
- document.close()
268
- }
269
- }
270
-
271
- // Restore focus after morphdom completes
272
- this.restoreFocusState(focusState)
273
- } else if (target === document.documentElement) {
274
- document.open()
275
- document.write(newHtml)
276
- document.close()
277
- }
278
- } finally {
279
- // Clear flag after a microtask to ensure all focus events are suppressed
280
- setTimeout(() => {
281
- DOMUpdater.isUpdating = false
282
- }, 0)
283
- }
284
- }
285
-
286
- /**
287
- * Update the DOM with new HTML content.
288
- */
289
- update(newHtml: string): void {
290
- const hasHtmlRoot = /<html[\s>]/i.test(newHtml)
291
- const hasBodyRoot = /<body[\s>]/i.test(newHtml)
292
- if (!hasHtmlRoot && document.body) {
293
- if (hasBodyRoot) {
294
- this.applyUpdate(document.body, newHtml)
295
- return
296
- }
297
- const wrapped = `<body>${newHtml}</body>`
298
- this.applyUpdate(document.body, wrapped)
299
- return
300
- }
301
- this.applyUpdate(document.documentElement, newHtml)
302
- }
303
-
304
- /**
305
- * Update a specific region by its region id.
306
- */
307
- updateRegion(regionId: string, regionHtml: string): void {
308
- const target = document.querySelector(`[data-pw-region="${regionId}"]`)
309
- if (!target) {
310
- if (this.debug) {
311
- console.warn('[DOMUpdater] Region not found:', regionId)
312
- }
313
- return
314
- }
315
- this.applyUpdate(target, regionHtml)
316
- // Ensure the region anchor remains even if the server HTML omitted it.
317
- if (!target.getAttribute('data-pw-region')) {
318
- target.setAttribute('data-pw-region', regionId)
319
- }
320
- }
321
- }
@@ -1,5 +0,0 @@
1
- // Core module exports
2
- export { PyWireApp, PyWireConfig } from './app'
3
- export { TransportManager, TransportConfig } from './transport-manager'
4
- export { DOMUpdater } from './dom-updater'
5
- export * from './transports'