pywire 0.1.0__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.
- pywire/__init__.py +2 -0
- pywire/cli/__init__.py +1 -0
- pywire/cli/generators.py +48 -0
- pywire/cli/main.py +309 -0
- pywire/cli/tui.py +563 -0
- pywire/cli/validate.py +26 -0
- pywire/client/.prettierignore +8 -0
- pywire/client/.prettierrc +7 -0
- pywire/client/build.mjs +73 -0
- pywire/client/eslint.config.js +46 -0
- pywire/client/package.json +39 -0
- pywire/client/pnpm-lock.yaml +2971 -0
- pywire/client/src/core/app.ts +263 -0
- pywire/client/src/core/dom-updater.test.ts +78 -0
- pywire/client/src/core/dom-updater.ts +321 -0
- pywire/client/src/core/index.ts +5 -0
- pywire/client/src/core/transport-manager.test.ts +179 -0
- pywire/client/src/core/transport-manager.ts +159 -0
- pywire/client/src/core/transports/base.ts +122 -0
- pywire/client/src/core/transports/http.ts +142 -0
- pywire/client/src/core/transports/index.ts +13 -0
- pywire/client/src/core/transports/websocket.ts +97 -0
- pywire/client/src/core/transports/webtransport.ts +149 -0
- pywire/client/src/dev/dev-app.ts +93 -0
- pywire/client/src/dev/error-trace.test.ts +97 -0
- pywire/client/src/dev/error-trace.ts +76 -0
- pywire/client/src/dev/index.ts +4 -0
- pywire/client/src/dev/status-overlay.ts +63 -0
- pywire/client/src/events/handler.test.ts +318 -0
- pywire/client/src/events/handler.ts +454 -0
- pywire/client/src/pywire.core.ts +22 -0
- pywire/client/src/pywire.dev.ts +27 -0
- pywire/client/tsconfig.json +17 -0
- pywire/client/vitest.config.ts +15 -0
- pywire/compiler/__init__.py +6 -0
- pywire/compiler/ast_nodes.py +304 -0
- pywire/compiler/attributes/__init__.py +6 -0
- pywire/compiler/attributes/base.py +24 -0
- pywire/compiler/attributes/conditional.py +37 -0
- pywire/compiler/attributes/events.py +55 -0
- pywire/compiler/attributes/form.py +37 -0
- pywire/compiler/attributes/loop.py +75 -0
- pywire/compiler/attributes/reactive.py +34 -0
- pywire/compiler/build.py +28 -0
- pywire/compiler/build_artifacts.py +342 -0
- pywire/compiler/codegen/__init__.py +5 -0
- pywire/compiler/codegen/attributes/__init__.py +6 -0
- pywire/compiler/codegen/attributes/base.py +19 -0
- pywire/compiler/codegen/attributes/events.py +35 -0
- pywire/compiler/codegen/directives/__init__.py +6 -0
- pywire/compiler/codegen/directives/base.py +16 -0
- pywire/compiler/codegen/directives/path.py +53 -0
- pywire/compiler/codegen/generator.py +2341 -0
- pywire/compiler/codegen/template.py +2178 -0
- pywire/compiler/directives/__init__.py +7 -0
- pywire/compiler/directives/base.py +20 -0
- pywire/compiler/directives/component.py +33 -0
- pywire/compiler/directives/context.py +93 -0
- pywire/compiler/directives/layout.py +49 -0
- pywire/compiler/directives/no_spa.py +24 -0
- pywire/compiler/directives/path.py +71 -0
- pywire/compiler/directives/props.py +88 -0
- pywire/compiler/exceptions.py +19 -0
- pywire/compiler/interpolation/__init__.py +6 -0
- pywire/compiler/interpolation/base.py +28 -0
- pywire/compiler/interpolation/jinja.py +272 -0
- pywire/compiler/parser.py +750 -0
- pywire/compiler/paths.py +29 -0
- pywire/compiler/preprocessor.py +43 -0
- pywire/core/wire.py +119 -0
- pywire/py.typed +0 -0
- pywire/runtime/__init__.py +7 -0
- pywire/runtime/aioquic_server.py +194 -0
- pywire/runtime/app.py +889 -0
- pywire/runtime/compile_error_page.py +195 -0
- pywire/runtime/debug.py +203 -0
- pywire/runtime/dev_server.py +434 -0
- pywire/runtime/dev_server.py.broken +268 -0
- pywire/runtime/error_page.py +64 -0
- pywire/runtime/error_renderer.py +23 -0
- pywire/runtime/escape.py +23 -0
- pywire/runtime/files.py +40 -0
- pywire/runtime/helpers.py +97 -0
- pywire/runtime/http_transport.py +253 -0
- pywire/runtime/loader.py +272 -0
- pywire/runtime/logging.py +72 -0
- pywire/runtime/page.py +384 -0
- pywire/runtime/pydantic_integration.py +52 -0
- pywire/runtime/router.py +229 -0
- pywire/runtime/server.py +25 -0
- pywire/runtime/style_collector.py +31 -0
- pywire/runtime/upload_manager.py +76 -0
- pywire/runtime/validation.py +449 -0
- pywire/runtime/websocket.py +665 -0
- pywire/runtime/webtransport_handler.py +195 -0
- pywire/templates/error/404.html +11 -0
- pywire/templates/error/500.html +38 -0
- pywire/templates/error/base.html +207 -0
- pywire/templates/error/compile_error.html +31 -0
- pywire-0.1.0.dist-info/METADATA +50 -0
- pywire-0.1.0.dist-info/RECORD +104 -0
- pywire-0.1.0.dist-info/WHEEL +4 -0
- pywire-0.1.0.dist-info/entry_points.txt +2 -0
- pywire-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { BaseTransport, ServerMessage } from './base'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WebTransport implementation using the browser's native WebTransport API.
|
|
5
|
+
* WebTransport provides lower latency than WebSocket via HTTP/3 and QUIC.
|
|
6
|
+
*/
|
|
7
|
+
export class WebTransportTransport extends BaseTransport {
|
|
8
|
+
readonly name = 'WebTransport'
|
|
9
|
+
|
|
10
|
+
private transport: WebTransport | null = null
|
|
11
|
+
private writer: WritableStreamDefaultWriter<Uint8Array> | null = null
|
|
12
|
+
private readonly url: string
|
|
13
|
+
private encoder = new TextEncoder()
|
|
14
|
+
private decoder = new TextDecoder()
|
|
15
|
+
|
|
16
|
+
constructor(url?: string) {
|
|
17
|
+
super()
|
|
18
|
+
this.url = url || this.getDefaultUrl()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private getDefaultUrl(): string {
|
|
22
|
+
// WebTransport requires HTTPS
|
|
23
|
+
return `https://${window.location.host}/_pywire/webtransport`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if WebTransport is available in this browser.
|
|
28
|
+
*/
|
|
29
|
+
static isSupported(): boolean {
|
|
30
|
+
return typeof WebTransport !== 'undefined'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async connect(): Promise<void> {
|
|
34
|
+
if (!WebTransportTransport.isSupported()) {
|
|
35
|
+
throw new Error('WebTransport not supported in this browser')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// Check for self-signed cert hash (Dev Mode)
|
|
40
|
+
const options: WebTransportOptions = {}
|
|
41
|
+
const certHash = (window as Window & { PYWIRE_CERT_HASH?: number[] }).PYWIRE_CERT_HASH
|
|
42
|
+
if (certHash && Array.isArray(certHash)) {
|
|
43
|
+
options.serverCertificateHashes = [
|
|
44
|
+
{
|
|
45
|
+
algorithm: 'sha-256',
|
|
46
|
+
value: new Uint8Array(certHash).buffer,
|
|
47
|
+
},
|
|
48
|
+
]
|
|
49
|
+
console.log('PyWire: Using explicit certificate hash for WebTransport')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.transport = new WebTransport(this.url, options)
|
|
53
|
+
|
|
54
|
+
// Wait for the connection to be ready
|
|
55
|
+
await this.transport.ready
|
|
56
|
+
|
|
57
|
+
console.log('PyWire: WebTransport ready')
|
|
58
|
+
this.connected = true
|
|
59
|
+
|
|
60
|
+
// Start reading incoming streams
|
|
61
|
+
this.startReading()
|
|
62
|
+
} catch (e) {
|
|
63
|
+
this.handleDisconnect()
|
|
64
|
+
throw e
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private async startReading(): Promise<void> {
|
|
69
|
+
if (!this.transport) return
|
|
70
|
+
|
|
71
|
+
// Read from bidirectional streams
|
|
72
|
+
const reader = this.transport.incomingBidirectionalStreams.getReader()
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
while (true) {
|
|
76
|
+
const { value: stream, done } = await reader.read()
|
|
77
|
+
if (done) break
|
|
78
|
+
|
|
79
|
+
// Handle each incoming stream
|
|
80
|
+
this.handleStream(stream)
|
|
81
|
+
}
|
|
82
|
+
} catch (e) {
|
|
83
|
+
if (this.connected) {
|
|
84
|
+
console.error('PyWire: WebTransport read error', e)
|
|
85
|
+
this.handleDisconnect()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private async handleStream(stream: WebTransportBidirectionalStream): Promise<void> {
|
|
91
|
+
const reader = stream.readable.getReader()
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
while (true) {
|
|
95
|
+
const { value, done } = await reader.read()
|
|
96
|
+
if (done) break
|
|
97
|
+
|
|
98
|
+
if (value) {
|
|
99
|
+
const text = this.decoder.decode(value)
|
|
100
|
+
try {
|
|
101
|
+
const msg = JSON.parse(text) as ServerMessage
|
|
102
|
+
this.notifyHandlers(msg)
|
|
103
|
+
} catch (e) {
|
|
104
|
+
console.error('PyWire: Error parsing WebTransport message', e)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch (e) {
|
|
109
|
+
console.error('PyWire: Stream read error', e)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async send(message: object): Promise<void> {
|
|
114
|
+
if (!this.transport || !this.connected) {
|
|
115
|
+
console.warn('PyWire: Cannot send message, WebTransport not connected')
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
// Create a new bidirectional stream for each message
|
|
121
|
+
const stream = await this.transport.createBidirectionalStream()
|
|
122
|
+
const writer = stream.writable.getWriter()
|
|
123
|
+
|
|
124
|
+
const data = this.encoder.encode(JSON.stringify(message))
|
|
125
|
+
await writer.write(data)
|
|
126
|
+
await writer.close()
|
|
127
|
+
|
|
128
|
+
// Read the response from this stream
|
|
129
|
+
this.handleStream(stream)
|
|
130
|
+
} catch (e) {
|
|
131
|
+
console.error('PyWire: WebTransport send error', e)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
disconnect(): void {
|
|
136
|
+
if (this.transport) {
|
|
137
|
+
this.transport.close()
|
|
138
|
+
this.transport = null
|
|
139
|
+
}
|
|
140
|
+
this.writer = null
|
|
141
|
+
this.connected = false
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private handleDisconnect(): void {
|
|
145
|
+
this.connected = false
|
|
146
|
+
this.transport = null
|
|
147
|
+
this.writer = null
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { PyWireApp, ServerMessage } from '../core'
|
|
2
|
+
import { StatusOverlay } from './status-overlay'
|
|
3
|
+
import { ErrorTraceHandler } from './error-trace'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Development-mode PyWire Application.
|
|
7
|
+
* Extends core app with:
|
|
8
|
+
* - Connection status overlay
|
|
9
|
+
* - Error trace handling with source loading
|
|
10
|
+
* - Enhanced console output
|
|
11
|
+
*/
|
|
12
|
+
export class PyWireDevApp extends PyWireApp {
|
|
13
|
+
private overlay: StatusOverlay | null = null
|
|
14
|
+
private errorHandler: ErrorTraceHandler
|
|
15
|
+
|
|
16
|
+
constructor(config = {}) {
|
|
17
|
+
super(config)
|
|
18
|
+
this.errorHandler = new ErrorTraceHandler()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async init(): Promise<void> {
|
|
22
|
+
// Create overlay before init so status changes are captured
|
|
23
|
+
this.overlay = new StatusOverlay()
|
|
24
|
+
|
|
25
|
+
await super.init()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Handle connection status changes with UI overlay.
|
|
30
|
+
*/
|
|
31
|
+
protected handleStatusChange(connected: boolean): void {
|
|
32
|
+
super.handleStatusChange(connected)
|
|
33
|
+
if (this.overlay) {
|
|
34
|
+
this.overlay.update(connected)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Navigate with offline feedback.
|
|
40
|
+
*/
|
|
41
|
+
navigateTo(path: string): void {
|
|
42
|
+
if (!this.isConnected) {
|
|
43
|
+
console.warn('PyWire: Navigation blocked - Offline')
|
|
44
|
+
if (this.overlay) {
|
|
45
|
+
this.overlay.showNavigationBlocked()
|
|
46
|
+
}
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
super.navigateTo(path)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Handle incoming server messages with enhanced error trace support.
|
|
55
|
+
*/
|
|
56
|
+
protected async handleMessage(msg: ServerMessage): Promise<void> {
|
|
57
|
+
switch (msg.type) {
|
|
58
|
+
case 'error_trace':
|
|
59
|
+
if (msg.trace) {
|
|
60
|
+
await this.errorHandler.handle(msg.error || 'Unknown Error', msg.trace)
|
|
61
|
+
}
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
case 'console':
|
|
65
|
+
// Enhanced console output with grouping
|
|
66
|
+
if (msg.lines && msg.lines.length > 0) {
|
|
67
|
+
const prefix = 'PyWire Server:'
|
|
68
|
+
const joined = msg.lines.join('\n')
|
|
69
|
+
if (msg.level === 'error') {
|
|
70
|
+
console.group(prefix + ' Error')
|
|
71
|
+
console.error(joined)
|
|
72
|
+
console.groupEnd()
|
|
73
|
+
} else if (msg.level === 'warn') {
|
|
74
|
+
console.groupCollapsed(prefix + ' Warning')
|
|
75
|
+
console.warn(joined)
|
|
76
|
+
console.groupEnd()
|
|
77
|
+
} else {
|
|
78
|
+
if (msg.lines.length === 1) {
|
|
79
|
+
console.log(prefix, joined)
|
|
80
|
+
} else {
|
|
81
|
+
console.groupCollapsed(prefix + ' Log')
|
|
82
|
+
console.log(joined)
|
|
83
|
+
console.groupEnd()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
default:
|
|
90
|
+
await super.handleMessage(msg)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
import { ErrorTraceHandler } from './error-trace'
|
|
3
|
+
|
|
4
|
+
describe('ErrorTraceHandler', () => {
|
|
5
|
+
let handler: ErrorTraceHandler
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.clearAllMocks()
|
|
9
|
+
handler = new ErrorTraceHandler()
|
|
10
|
+
vi.stubGlobal('location', { origin: 'http://localhost:8000' })
|
|
11
|
+
vi.stubGlobal('fetch', vi.fn())
|
|
12
|
+
vi.stubGlobal('console', { error: vi.fn(), warn: vi.fn(), log: vi.fn() })
|
|
13
|
+
// vi.stubGlobal('eval', vi.fn()); // indirect eval is hard to mock, but we can check if it's called
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('should generate virtual URLs correctly', () => {
|
|
17
|
+
// getVirtualUrl is private, so we'll test it through handle()'s side effects or by casting
|
|
18
|
+
const virtualUrl = (
|
|
19
|
+
handler as unknown as { getVirtualUrl: (filename: string) => string }
|
|
20
|
+
).getVirtualUrl('/path/to/script.py')
|
|
21
|
+
expect(virtualUrl).toContain('http://localhost:8000/_pywire/file/')
|
|
22
|
+
expect(virtualUrl).toContain('script.py')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('should fetch source files and inject them with sourceURL', async () => {
|
|
26
|
+
const fetchMock = vi.mocked(fetch)
|
|
27
|
+
fetchMock.mockResolvedValue({
|
|
28
|
+
ok: true,
|
|
29
|
+
text: async () => 'print("hello")',
|
|
30
|
+
} as Response)
|
|
31
|
+
|
|
32
|
+
const trace = [
|
|
33
|
+
{
|
|
34
|
+
filename: '/path/to/script.py',
|
|
35
|
+
lineno: 10,
|
|
36
|
+
colno: 5,
|
|
37
|
+
name: 'my_func',
|
|
38
|
+
line: 'def my_func():',
|
|
39
|
+
},
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
await handler.handle('Test Error', trace)
|
|
43
|
+
|
|
44
|
+
expect(fetchMock).toHaveBeenCalledWith('/_pywire/source?path=%2Fpath%2Fto%2Fscript.py')
|
|
45
|
+
// We expect console.error to have been called with the constructed stack
|
|
46
|
+
expect(vi.mocked(console.error)).toHaveBeenCalledWith(
|
|
47
|
+
expect.stringContaining('at my_func (http://localhost:8000/_pywire/file/')
|
|
48
|
+
)
|
|
49
|
+
expect(vi.mocked(console.error)).toHaveBeenCalledWith(expect.stringContaining(':10:5)'))
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should handle missing column numbers', async () => {
|
|
53
|
+
vi.mocked(fetch).mockResolvedValue({ ok: false } as Response)
|
|
54
|
+
|
|
55
|
+
const trace = [{ filename: 'test.py', lineno: 5, name: 'foo', line: 'x = 1' }]
|
|
56
|
+
|
|
57
|
+
await handler.handle('Error', trace)
|
|
58
|
+
|
|
59
|
+
expect(vi.mocked(console.error)).toHaveBeenCalledWith(
|
|
60
|
+
expect.stringContaining('at foo (http://localhost:8000/_pywire/file/')
|
|
61
|
+
)
|
|
62
|
+
expect(vi.mocked(console.error)).toHaveBeenCalledWith(
|
|
63
|
+
expect.stringContaining(':5:1)') // Default colno is 1
|
|
64
|
+
)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should not reload already loaded sources', async () => {
|
|
68
|
+
const fetchMock = vi.mocked(fetch)
|
|
69
|
+
fetchMock.mockResolvedValue({
|
|
70
|
+
ok: true,
|
|
71
|
+
text: async () => 'content',
|
|
72
|
+
} as Response)
|
|
73
|
+
|
|
74
|
+
const trace = [{ filename: 'test.py', lineno: 1, name: 'foo', line: 'content' }]
|
|
75
|
+
|
|
76
|
+
await handler.handle('Err 1', trace)
|
|
77
|
+
await handler.handle('Err 2', trace)
|
|
78
|
+
|
|
79
|
+
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should handle fetch failures gracefully', async () => {
|
|
83
|
+
vi.mocked(fetch).mockRejectedValue(new Error('Network error'))
|
|
84
|
+
|
|
85
|
+
const trace = [{ filename: 'test.py', lineno: 1, name: 'foo', line: 'content' }]
|
|
86
|
+
|
|
87
|
+
await handler.handle('Err', trace)
|
|
88
|
+
|
|
89
|
+
expect(vi.mocked(console.warn)).toHaveBeenCalledWith(
|
|
90
|
+
expect.stringContaining('PyWire: Failed to load source'),
|
|
91
|
+
'test.py',
|
|
92
|
+
expect.any(Error)
|
|
93
|
+
)
|
|
94
|
+
// Should still log the error stack
|
|
95
|
+
expect(vi.mocked(console.error)).toHaveBeenCalled()
|
|
96
|
+
})
|
|
97
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { StackFrame } from '../core/transports'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Handles error traces from the server in development mode.
|
|
5
|
+
* Loads source files and displays errors with proper source mapping in DevTools.
|
|
6
|
+
*/
|
|
7
|
+
export class ErrorTraceHandler {
|
|
8
|
+
private loadedSources = new Set<string>()
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get a virtual URL for a filename that Chrome will display in stack traces.
|
|
12
|
+
*/
|
|
13
|
+
private getVirtualUrl(filename: string): string {
|
|
14
|
+
const encoded = btoa(filename).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
|
15
|
+
const cleanName = filename.split(/[/\\]/).pop() || 'unknown'
|
|
16
|
+
return `${window.location.origin}/_pywire/file/${encoded}/${cleanName}`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Handle an error trace message from the server.
|
|
21
|
+
*/
|
|
22
|
+
async handle(errorMessage: string, trace: StackFrame[]): Promise<void> {
|
|
23
|
+
// Load sources for frames
|
|
24
|
+
const filesToLoad = new Set<string>()
|
|
25
|
+
for (const frame of trace) {
|
|
26
|
+
if (!this.loadedSources.has(frame.filename)) {
|
|
27
|
+
filesToLoad.add(frame.filename)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await Promise.all(
|
|
32
|
+
Array.from(filesToLoad).map(async (filename) => {
|
|
33
|
+
try {
|
|
34
|
+
const virtualUrl = this.getVirtualUrl(filename)
|
|
35
|
+
|
|
36
|
+
// Fetch content
|
|
37
|
+
const url = `/_pywire/source?path=${encodeURIComponent(filename)}`
|
|
38
|
+
const resp = await fetch(url)
|
|
39
|
+
if (resp.ok) {
|
|
40
|
+
const content = await resp.text()
|
|
41
|
+
|
|
42
|
+
// Inject the raw source with sourceURL for DevTools
|
|
43
|
+
const sourceCode = `${content}\n//# sourceURL=${virtualUrl}`
|
|
44
|
+
try {
|
|
45
|
+
;(0, eval)(sourceCode)
|
|
46
|
+
} catch {
|
|
47
|
+
// Syntax errors from Python content are expected - ignore
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.loadedSources.add(filename)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
this.loadedSources.add(filename)
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.warn('PyWire: Failed to load source', filename, e)
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
// Construct Error with stack pointing to virtual URLs
|
|
61
|
+
const err = new Error(errorMessage)
|
|
62
|
+
const stackLines = [`${err.name}: ${err.message}`]
|
|
63
|
+
|
|
64
|
+
for (const frame of trace) {
|
|
65
|
+
const fn = frame.name || '<module>'
|
|
66
|
+
const virtualUrl = this.getVirtualUrl(frame.filename)
|
|
67
|
+
const col = frame.colno ?? 1
|
|
68
|
+
stackLines.push(` at ${fn} (${virtualUrl}:${frame.lineno}:${col})`)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
err.stack = stackLines.join('\n')
|
|
72
|
+
|
|
73
|
+
// Log just the stack string to avoid Chrome appending its own call stack
|
|
74
|
+
console.error(err.stack)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection status overlay for development mode.
|
|
3
|
+
* Shows a toast-style notification when connection is lost.
|
|
4
|
+
*/
|
|
5
|
+
export class StatusOverlay {
|
|
6
|
+
private element: HTMLElement | null = null
|
|
7
|
+
|
|
8
|
+
constructor() {
|
|
9
|
+
this.create()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
private create(): void {
|
|
13
|
+
this.element = document.createElement('div')
|
|
14
|
+
this.element.style.cssText = `
|
|
15
|
+
position: fixed;
|
|
16
|
+
bottom: 20px;
|
|
17
|
+
right: 20px;
|
|
18
|
+
background: rgba(0, 0, 0, 0.8);
|
|
19
|
+
color: white;
|
|
20
|
+
padding: 10px 20px;
|
|
21
|
+
border-radius: 5px;
|
|
22
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
23
|
+
font-size: 14px;
|
|
24
|
+
z-index: 10000;
|
|
25
|
+
display: none;
|
|
26
|
+
transition: opacity 0.3s;
|
|
27
|
+
pointer-events: none;
|
|
28
|
+
`
|
|
29
|
+
document.body.appendChild(this.element)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Update overlay based on connection status.
|
|
34
|
+
*/
|
|
35
|
+
update(connected: boolean): void {
|
|
36
|
+
if (!this.element) return
|
|
37
|
+
|
|
38
|
+
if (connected) {
|
|
39
|
+
this.element.style.display = 'none'
|
|
40
|
+
} else {
|
|
41
|
+
this.element.textContent = 'Connection Lost - Reconnecting...'
|
|
42
|
+
this.element.style.display = 'block'
|
|
43
|
+
this.element.style.backgroundColor = 'rgba(0, 0, 0, 0.8)'
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Show a temporary navigation-blocked message.
|
|
49
|
+
*/
|
|
50
|
+
showNavigationBlocked(): void {
|
|
51
|
+
if (!this.element) return
|
|
52
|
+
|
|
53
|
+
this.element.style.backgroundColor = 'rgba(200, 0, 0, 0.9)'
|
|
54
|
+
this.element.textContent = 'Cannot navigate - Offline'
|
|
55
|
+
this.element.style.display = 'block'
|
|
56
|
+
|
|
57
|
+
setTimeout(() => {
|
|
58
|
+
if (this.element) {
|
|
59
|
+
this.element.style.backgroundColor = 'rgba(0, 0, 0, 0.8)'
|
|
60
|
+
}
|
|
61
|
+
}, 1500)
|
|
62
|
+
}
|
|
63
|
+
}
|