@bitstillery/mithril 3.0.0-AA
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/LICENSE +21 -0
- package/README.md +185 -0
- package/api/mount-redraw.ts +173 -0
- package/api/router-server.ts +88 -0
- package/api/router.ts +670 -0
- package/browser.ts +10 -0
- package/htm.ts +7 -0
- package/hyperscript.ts +8 -0
- package/index.ts +134 -0
- package/jsx.d.ts +16 -0
- package/mount.ts +10 -0
- package/package.json +66 -0
- package/pathname/build.ts +40 -0
- package/pathname/compileTemplate.ts +49 -0
- package/pathname/parse.ts +19 -0
- package/querystring/build.ts +22 -0
- package/querystring/parse.ts +50 -0
- package/redraw.ts +10 -0
- package/render/cachedAttrsIsStaticMap.ts +10 -0
- package/render/delayedRemoval.ts +1 -0
- package/render/domFor.ts +23 -0
- package/render/emptyAttrs.ts +2 -0
- package/render/fragment.ts +11 -0
- package/render/hyperscript.ts +125 -0
- package/render/hyperscriptVnode.ts +21 -0
- package/render/render.ts +1307 -0
- package/render/renderToString.ts +494 -0
- package/render/ssrState.ts +366 -0
- package/render/trust.ts +6 -0
- package/render/vnode.ts +131 -0
- package/route.ts +11 -0
- package/server/logger.ts +223 -0
- package/server/session.ts +87 -0
- package/server/ssr.ts +205 -0
- package/server/ssrLogger.ts +13 -0
- package/server.ts +101 -0
- package/signal.ts +320 -0
- package/ssrContext.ts +87 -0
- package/state.ts +1026 -0
- package/store.ts +457 -0
- package/util/censor.ts +46 -0
- package/util/decodeURIComponentSafe.ts +34 -0
- package/util/hasOwn.ts +2 -0
- package/util/next_tick.ts +45 -0
- package/util/ssr.ts +385 -0
- package/util/uri.ts +126 -0
- package/version.ts +3 -0
package/util/ssr.ts
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
// SSR and hydration utilities
|
|
2
|
+
|
|
3
|
+
import {logger} from '../server/logger'
|
|
4
|
+
|
|
5
|
+
// Development-only hydration debugging
|
|
6
|
+
export const HYDRATION_DEBUG = typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production'
|
|
7
|
+
|
|
8
|
+
// Throttle hydration error logging to avoid performance issues
|
|
9
|
+
let hydrationErrorCount = 0
|
|
10
|
+
const MAX_HYDRATION_ERRORS = 10 // Limit number of errors logged per render cycle
|
|
11
|
+
|
|
12
|
+
// Reset error count at the start of each render cycle
|
|
13
|
+
export function resetHydrationErrorCount(): void {
|
|
14
|
+
hydrationErrorCount = 0
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getComponentName(vnode: any): string {
|
|
18
|
+
if (!vnode) return 'Unknown'
|
|
19
|
+
if (typeof vnode.tag === 'string') return vnode.tag
|
|
20
|
+
if (vnode.tag?.name) return vnode.tag.name
|
|
21
|
+
if (vnode.tag?.displayName) return vnode.tag.displayName
|
|
22
|
+
if (vnode.state?.constructor?.name) return vnode.state.constructor.name
|
|
23
|
+
return 'Unknown'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Format a DOM element as an opening tag string
|
|
27
|
+
function formatDOMElement(el: Element): {tagName: string; openTag: string; closeTag: string} {
|
|
28
|
+
const tagName = el.tagName.toLowerCase()
|
|
29
|
+
let openTag = `<${tagName}`
|
|
30
|
+
|
|
31
|
+
// Add important attributes
|
|
32
|
+
if (el.id) {
|
|
33
|
+
openTag += ` id="${el.id}"`
|
|
34
|
+
}
|
|
35
|
+
if (el.className && typeof el.className === 'string') {
|
|
36
|
+
const classes = el.className
|
|
37
|
+
.split(' ')
|
|
38
|
+
.filter((c) => c)
|
|
39
|
+
.slice(0, 3)
|
|
40
|
+
.join(' ')
|
|
41
|
+
if (classes) {
|
|
42
|
+
openTag += ` className="${classes}${el.className.split(' ').length > 3 ? '...' : ''}"`
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
openTag += '>'
|
|
47
|
+
return {tagName, openTag, closeTag: `</${tagName}>`}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function formatVDOMTree(
|
|
51
|
+
vnode: any,
|
|
52
|
+
maxDepth: number = 6,
|
|
53
|
+
currentDepth: number = 0,
|
|
54
|
+
showComponentInstance: boolean = true,
|
|
55
|
+
): string {
|
|
56
|
+
if (!vnode || currentDepth >= maxDepth) return ''
|
|
57
|
+
|
|
58
|
+
const indent = ' '.repeat(currentDepth)
|
|
59
|
+
|
|
60
|
+
// Handle text nodes
|
|
61
|
+
if (vnode.tag === '#') {
|
|
62
|
+
const text = String(vnode.children || vnode.text || '').substring(0, 50)
|
|
63
|
+
return `${indent}"${text}${String(vnode.children || vnode.text || '').length > 50 ? '...' : ''}"`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Handle fragments
|
|
67
|
+
if (vnode.tag === '[') {
|
|
68
|
+
if (!vnode.children || !Array.isArray(vnode.children) || vnode.children.length === 0) {
|
|
69
|
+
return `${indent}[fragment]`
|
|
70
|
+
}
|
|
71
|
+
const validChildren = vnode.children.filter((c: any) => c != null).slice(0, 8)
|
|
72
|
+
let result = `${indent}[fragment]\n`
|
|
73
|
+
for (const child of validChildren) {
|
|
74
|
+
result += formatVDOMTree(child, maxDepth, currentDepth + 1, showComponentInstance) + '\n'
|
|
75
|
+
}
|
|
76
|
+
if (vnode.children.filter((c: any) => c != null).length > 8) {
|
|
77
|
+
result += `${indent} ... (${vnode.children.filter((c: any) => c != null).length - 8} more)\n`
|
|
78
|
+
}
|
|
79
|
+
return result.trimEnd()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const isComponent = typeof vnode.tag !== 'string'
|
|
83
|
+
const tagName = isComponent ? getComponentName(vnode) : vnode.tag
|
|
84
|
+
|
|
85
|
+
let result = `${indent}<${tagName}`
|
|
86
|
+
|
|
87
|
+
// Add key if present
|
|
88
|
+
if (vnode.attrs?.key) {
|
|
89
|
+
result += ` key="${vnode.attrs.key}"`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Add a few important attributes for debugging
|
|
93
|
+
if (vnode.attrs) {
|
|
94
|
+
const importantAttrs = ['id', 'class', 'className']
|
|
95
|
+
for (const attr of importantAttrs) {
|
|
96
|
+
if (vnode.attrs[attr]) {
|
|
97
|
+
const value = typeof vnode.attrs[attr] === 'string' ? vnode.attrs[attr] : String(vnode.attrs[attr])
|
|
98
|
+
result += ` ${attr}="${value.substring(0, 30)}${value.length > 30 ? '...' : ''}"`
|
|
99
|
+
break // Only show first important attr to keep it concise
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
result += '>'
|
|
105
|
+
|
|
106
|
+
// For components, show their rendered instance (what the component actually renders)
|
|
107
|
+
// This gives us parent context without performance cost
|
|
108
|
+
if (isComponent && showComponentInstance && vnode.instance && currentDepth < maxDepth - 1) {
|
|
109
|
+
const instanceTree = formatVDOMTree(vnode.instance, maxDepth, currentDepth + 1, showComponentInstance)
|
|
110
|
+
if (instanceTree) {
|
|
111
|
+
result += '\n' + instanceTree
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Add children
|
|
116
|
+
if (vnode.children && Array.isArray(vnode.children) && currentDepth < maxDepth - 1) {
|
|
117
|
+
const validChildren = vnode.children.filter((c: any) => c != null).slice(0, 10)
|
|
118
|
+
if (validChildren.length > 0) {
|
|
119
|
+
result += '\n'
|
|
120
|
+
for (const child of validChildren) {
|
|
121
|
+
if (typeof child === 'string' || typeof child === 'number') {
|
|
122
|
+
const text = String(child).substring(0, 50)
|
|
123
|
+
result += `${indent} "${text}${String(child).length > 50 ? '...' : ''}"\n`
|
|
124
|
+
} else {
|
|
125
|
+
const childTree = formatVDOMTree(child, maxDepth, currentDepth + 1, showComponentInstance)
|
|
126
|
+
if (childTree) {
|
|
127
|
+
result += childTree + '\n'
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (vnode.children.filter((c: any) => c != null).length > 10) {
|
|
132
|
+
result += `${indent} ... (${vnode.children.filter((c: any) => c != null).length - 10} more children)\n`
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} else if (vnode.text != null) {
|
|
136
|
+
const text = String(vnode.text).substring(0, 50)
|
|
137
|
+
result += ` "${text}${String(vnode.text).length > 50 ? '...' : ''}"`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
result += `${indent}</${tagName}>`
|
|
141
|
+
|
|
142
|
+
return result
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Combine DOM parent chain with VDOM structure into a single HTML-like tree
|
|
146
|
+
function formatCombinedStructure(parent: Element | Node | null, vnode: any, maxParents: number = 4): string {
|
|
147
|
+
if (!parent && !vnode) return ''
|
|
148
|
+
|
|
149
|
+
// Collect DOM parents (from outermost to innermost)
|
|
150
|
+
const domElements: {openTag: string; closeTag: string}[] = []
|
|
151
|
+
let current: Node | null = parent
|
|
152
|
+
let depth = 0
|
|
153
|
+
|
|
154
|
+
while (current && depth < maxParents) {
|
|
155
|
+
if (current.nodeType === 1) {
|
|
156
|
+
// Element node
|
|
157
|
+
const el = current as Element
|
|
158
|
+
// Skip html and body tags - they're not useful context
|
|
159
|
+
if (el.tagName !== 'HTML' && el.tagName !== 'BODY') {
|
|
160
|
+
domElements.unshift(formatDOMElement(el))
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
current = current.parentElement || current.parentNode
|
|
164
|
+
depth++
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Build the combined output
|
|
168
|
+
const lines: string[] = []
|
|
169
|
+
|
|
170
|
+
// Opening tags for DOM parents
|
|
171
|
+
domElements.forEach((el, i) => {
|
|
172
|
+
lines.push(' '.repeat(i) + el.openTag)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
// VDOM structure (indented inside the DOM parents)
|
|
176
|
+
if (vnode) {
|
|
177
|
+
const vdomIndent = domElements.length
|
|
178
|
+
const vdomTree = formatVDOMTree(vnode, 4, 0, true)
|
|
179
|
+
if (vdomTree) {
|
|
180
|
+
// Indent each line of the VDOM tree
|
|
181
|
+
const vdomLines = vdomTree.split('\n')
|
|
182
|
+
vdomLines.forEach((line) => {
|
|
183
|
+
lines.push(' '.repeat(vdomIndent) + line)
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Closing tags for DOM parents (in reverse order)
|
|
189
|
+
for (let i = domElements.length - 1; i >= 0; i--) {
|
|
190
|
+
lines.push(' '.repeat(i) + domElements[i].closeTag)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return lines.join('\n')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function buildComponentPath(vnode: any, context?: {oldVnode?: any; newVnode?: any}): string[] {
|
|
197
|
+
const path: string[] = []
|
|
198
|
+
|
|
199
|
+
const traverseVnode = (v: any, depth: number = 0): boolean => {
|
|
200
|
+
if (!v || depth > 10) return false
|
|
201
|
+
|
|
202
|
+
const name = getComponentName(v)
|
|
203
|
+
const isComponent =
|
|
204
|
+
typeof v.tag !== 'string' && name !== 'Unknown' && name !== 'Component' && name !== 'AnonymousComponent'
|
|
205
|
+
|
|
206
|
+
if (isComponent) {
|
|
207
|
+
path.push(name)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (v.instance && depth < 2) {
|
|
211
|
+
if (traverseVnode(v.instance, depth + 1)) return true
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (v.children && Array.isArray(v.children) && depth < 2) {
|
|
215
|
+
for (let i = 0; i < Math.min(v.children.length, 3); i++) {
|
|
216
|
+
const child = v.children[i]
|
|
217
|
+
if (child && traverseVnode(child, depth + 1)) return true
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return false
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (context?.newVnode) {
|
|
225
|
+
traverseVnode(context.newVnode)
|
|
226
|
+
if (path.length > 0) return path
|
|
227
|
+
}
|
|
228
|
+
if (context?.oldVnode) {
|
|
229
|
+
traverseVnode(context.oldVnode)
|
|
230
|
+
if (path.length > 0) return path
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (vnode) {
|
|
234
|
+
traverseVnode(vnode)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return path
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function formatComponentHierarchy(vnode: any, context?: {oldVnode?: any; newVnode?: any}): string {
|
|
241
|
+
if (!vnode) return 'Unknown'
|
|
242
|
+
|
|
243
|
+
const path = buildComponentPath(vnode, context)
|
|
244
|
+
const immediateName = getComponentName(vnode)
|
|
245
|
+
const isElement = typeof vnode.tag === 'string'
|
|
246
|
+
|
|
247
|
+
if (path.length > 0) {
|
|
248
|
+
const pathStr = path.join(' → ')
|
|
249
|
+
if (isElement && immediateName !== path[path.length - 1]) {
|
|
250
|
+
return `${immediateName} in ${pathStr}`
|
|
251
|
+
} else {
|
|
252
|
+
return pathStr
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return immediateName
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export interface HydrationErrorContext {
|
|
260
|
+
parent?: Element
|
|
261
|
+
node?: Node
|
|
262
|
+
matchedNodes?: Set<Node>
|
|
263
|
+
oldVnode?: any
|
|
264
|
+
newVnode?: any
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function logHydrationError(
|
|
268
|
+
operation: string,
|
|
269
|
+
vnode: any,
|
|
270
|
+
_element: Element | null,
|
|
271
|
+
error: Error,
|
|
272
|
+
context?: HydrationErrorContext,
|
|
273
|
+
): void {
|
|
274
|
+
// Update hydration statistics
|
|
275
|
+
updateHydrationStats(vnode)
|
|
276
|
+
|
|
277
|
+
// Throttle error logging to avoid performance issues
|
|
278
|
+
hydrationErrorCount++
|
|
279
|
+
if (hydrationErrorCount > MAX_HYDRATION_ERRORS) {
|
|
280
|
+
if (hydrationErrorCount === MAX_HYDRATION_ERRORS + 1) {
|
|
281
|
+
const topComponents = Array.from(hydrationStats.componentMismatches.entries())
|
|
282
|
+
.toSorted((a: [string, number], b: [string, number]) => b[1] - a[1])
|
|
283
|
+
.slice(0, 5)
|
|
284
|
+
.map(([name, count]: [string, number]) => `${name}: ${count}`)
|
|
285
|
+
.join(', ')
|
|
286
|
+
|
|
287
|
+
logger.warn(
|
|
288
|
+
`hydration errors throttled: more than ${MAX_HYDRATION_ERRORS} errors detected. suppressing further logs to improve performance.`,
|
|
289
|
+
{
|
|
290
|
+
totalMismatches: hydrationStats.totalMismatches,
|
|
291
|
+
topComponents: topComponents || 'none',
|
|
292
|
+
},
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Build user-friendly component hierarchy
|
|
299
|
+
const componentHierarchy = formatComponentHierarchy(vnode, context)
|
|
300
|
+
|
|
301
|
+
// Log hydration error with structured context
|
|
302
|
+
const logContext: Record<string, any> = {
|
|
303
|
+
componentPath: componentHierarchy,
|
|
304
|
+
operation,
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (context?.node) {
|
|
308
|
+
logContext.affectedNode = context.node.nodeType === 1 ? `${(context.node as Element).tagName.toLowerCase()}` : 'text'
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Include structure info in debug mode
|
|
312
|
+
if (HYDRATION_DEBUG) {
|
|
313
|
+
const vnodeToShow = context?.oldVnode || vnode || context?.newVnode
|
|
314
|
+
try {
|
|
315
|
+
const combinedStructure = formatCombinedStructure(context?.parent || null, vnodeToShow, 4)
|
|
316
|
+
if (combinedStructure) {
|
|
317
|
+
logContext.structure = combinedStructure
|
|
318
|
+
}
|
|
319
|
+
} catch (_e) {
|
|
320
|
+
// Fallback: try to show at least the VDOM structure
|
|
321
|
+
if (vnodeToShow) {
|
|
322
|
+
try {
|
|
323
|
+
const vdomTree = formatVDOMTree(vnodeToShow, 4, 0, true)
|
|
324
|
+
if (vdomTree) {
|
|
325
|
+
logContext.vdomStructure = vdomTree
|
|
326
|
+
}
|
|
327
|
+
} catch (_e2) {
|
|
328
|
+
logContext.component = getComponentName(vnodeToShow)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Show what's being removed vs what's replacing it (if both exist)
|
|
334
|
+
if (context?.oldVnode && context?.newVnode) {
|
|
335
|
+
try {
|
|
336
|
+
const oldTree = formatVDOMTree(context.oldVnode, 3)
|
|
337
|
+
const newTree = formatVDOMTree(context.newVnode, 3)
|
|
338
|
+
if (oldTree) logContext.removing = oldTree
|
|
339
|
+
if (newTree) logContext.replacingWith = newTree
|
|
340
|
+
} catch (_e) {
|
|
341
|
+
// Silently fail if formatting doesn't work
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (operation.includes('removeChild') || operation.includes('removeDOM')) {
|
|
347
|
+
logContext.handledGracefully = true
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
logger.error(`hydration error: ${operation}`, error, logContext)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Track hydration statistics for debugging
|
|
354
|
+
interface HydrationStats {
|
|
355
|
+
totalMismatches: number
|
|
356
|
+
componentMismatches: Map<string, number>
|
|
357
|
+
lastMismatchTime: number
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
let hydrationStats: HydrationStats = {
|
|
361
|
+
totalMismatches: 0,
|
|
362
|
+
componentMismatches: new Map(),
|
|
363
|
+
lastMismatchTime: 0,
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function getHydrationStats(): HydrationStats {
|
|
367
|
+
return {...hydrationStats, componentMismatches: new Map(hydrationStats.componentMismatches)}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function resetHydrationStats(): void {
|
|
371
|
+
hydrationStats = {
|
|
372
|
+
totalMismatches: 0,
|
|
373
|
+
componentMismatches: new Map(),
|
|
374
|
+
lastMismatchTime: 0,
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Update stats when hydration error occurs
|
|
379
|
+
function updateHydrationStats(vnode: any): void {
|
|
380
|
+
hydrationStats.totalMismatches++
|
|
381
|
+
hydrationStats.lastMismatchTime = Date.now()
|
|
382
|
+
const componentName = getComponentName(vnode)
|
|
383
|
+
const currentCount = hydrationStats.componentMismatches.get(componentName) || 0
|
|
384
|
+
hydrationStats.componentMismatches.set(componentName, currentCount + 1)
|
|
385
|
+
}
|
package/util/uri.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Isomorphic URI API - works in both SSR (server) and browser contexts
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get the current full URL (href)
|
|
7
|
+
* Returns window.location.href in browser, or server's request URL during SSR
|
|
8
|
+
*/
|
|
9
|
+
export function getCurrentUrl(): string {
|
|
10
|
+
// Prefer browser location when available (more direct)
|
|
11
|
+
if (typeof window !== 'undefined' && window.location) {
|
|
12
|
+
return window.location.href
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Fall back to SSR server URL (when client code runs on server)
|
|
16
|
+
if (typeof globalThis !== 'undefined' && (globalThis as any).__SSR_URL__) {
|
|
17
|
+
return (globalThis as any).__SSR_URL__
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Fallback (shouldn't happen)
|
|
21
|
+
return ''
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parse a URL string into its components
|
|
26
|
+
*/
|
|
27
|
+
function parseUrl(url: string): URL {
|
|
28
|
+
try {
|
|
29
|
+
return new URL(url)
|
|
30
|
+
} catch {
|
|
31
|
+
// Fallback for relative URLs
|
|
32
|
+
return new URL(url, 'http://localhost')
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get the current pathname
|
|
38
|
+
*/
|
|
39
|
+
export function getPathname(): string {
|
|
40
|
+
// Prefer browser location when available (more direct)
|
|
41
|
+
if (typeof window !== 'undefined' && window.location) {
|
|
42
|
+
return window.location.pathname || '/'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Fall back to parsing SSR URL
|
|
46
|
+
const url = getCurrentUrl()
|
|
47
|
+
if (!url) return '/'
|
|
48
|
+
|
|
49
|
+
const parsed = parseUrl(url)
|
|
50
|
+
return parsed.pathname || '/'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get the current search string (query string)
|
|
55
|
+
*/
|
|
56
|
+
export function getSearch(): string {
|
|
57
|
+
// Prefer browser location when available (more direct)
|
|
58
|
+
if (typeof window !== 'undefined' && window.location) {
|
|
59
|
+
return window.location.search || ''
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Fall back to parsing SSR URL
|
|
63
|
+
const url = getCurrentUrl()
|
|
64
|
+
if (!url) return ''
|
|
65
|
+
|
|
66
|
+
const parsed = parseUrl(url)
|
|
67
|
+
return parsed.search || ''
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get the current hash
|
|
72
|
+
*/
|
|
73
|
+
export function getHash(): string {
|
|
74
|
+
// Prefer browser location when available (more direct)
|
|
75
|
+
if (typeof window !== 'undefined' && window.location) {
|
|
76
|
+
return window.location.hash || ''
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Fall back to parsing SSR URL
|
|
80
|
+
const url = getCurrentUrl()
|
|
81
|
+
if (!url) return ''
|
|
82
|
+
|
|
83
|
+
const parsed = parseUrl(url)
|
|
84
|
+
return parsed.hash || ''
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get a Location-like object with all properties
|
|
89
|
+
* Compatible with browser Location API
|
|
90
|
+
*/
|
|
91
|
+
export interface IsomorphicLocation {
|
|
92
|
+
href: string
|
|
93
|
+
pathname: string
|
|
94
|
+
search: string
|
|
95
|
+
hash: string
|
|
96
|
+
origin?: string
|
|
97
|
+
host?: string
|
|
98
|
+
hostname?: string
|
|
99
|
+
port?: string
|
|
100
|
+
protocol?: string
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getLocation(): IsomorphicLocation {
|
|
104
|
+
const url = getCurrentUrl()
|
|
105
|
+
if (!url) {
|
|
106
|
+
return {
|
|
107
|
+
href: '',
|
|
108
|
+
pathname: '/',
|
|
109
|
+
search: '',
|
|
110
|
+
hash: '',
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const parsed = parseUrl(url)
|
|
115
|
+
return {
|
|
116
|
+
href: parsed.href,
|
|
117
|
+
pathname: parsed.pathname || '/',
|
|
118
|
+
search: parsed.search || '',
|
|
119
|
+
hash: parsed.hash || '',
|
|
120
|
+
origin: parsed.origin,
|
|
121
|
+
host: parsed.host,
|
|
122
|
+
hostname: parsed.hostname,
|
|
123
|
+
port: parsed.port,
|
|
124
|
+
protocol: parsed.protocol,
|
|
125
|
+
}
|
|
126
|
+
}
|
package/version.ts
ADDED