one 1.9.3 → 1.9.5
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/dist/cjs/cli/build.cjs +3 -1
- package/dist/cjs/cli/build.js +5 -1
- package/dist/cjs/cli/build.js.map +1 -1
- package/dist/cjs/cli/build.native.js +3 -1
- package/dist/cjs/cli/build.native.js.map +1 -1
- package/dist/cjs/cli/buildPageWorker.cjs +1 -1
- package/dist/cjs/cli/buildPageWorker.js +1 -1
- package/dist/cjs/cli/buildPageWorker.js.map +1 -1
- package/dist/cjs/cli/buildPageWorker.native.js +1 -1
- package/dist/cjs/cli/buildPageWorker.native.js.map +1 -1
- package/dist/cjs/daemon/picker.cjs +4 -0
- package/dist/cjs/daemon/picker.js +4 -0
- package/dist/cjs/daemon/picker.js.map +1 -1
- package/dist/cjs/daemon/picker.native.js +4 -0
- package/dist/cjs/daemon/picker.native.js.map +1 -1
- package/dist/cjs/daemon/tui.cjs +5 -1
- package/dist/cjs/daemon/tui.js +5 -1
- package/dist/cjs/daemon/tui.js.map +1 -1
- package/dist/cjs/daemon/tui.native.js +5 -1
- package/dist/cjs/daemon/tui.native.js.map +1 -1
- package/dist/cjs/router/router.cjs +4 -0
- package/dist/cjs/router/router.js +4 -0
- package/dist/cjs/router/router.js.map +1 -1
- package/dist/cjs/router/router.native.js +4 -0
- package/dist/cjs/router/router.native.js.map +1 -1
- package/dist/cjs/server/oneServe.cjs +42 -25
- package/dist/cjs/server/oneServe.js +45 -24
- package/dist/cjs/server/oneServe.js.map +1 -1
- package/dist/cjs/server/oneServe.native.js +41 -24
- package/dist/cjs/server/oneServe.native.js.map +1 -1
- package/dist/cjs/useLoader.cjs +43 -25
- package/dist/cjs/useLoader.js +30 -12
- package/dist/cjs/useLoader.js.map +1 -1
- package/dist/cjs/useLoader.native.js +54 -29
- package/dist/cjs/useLoader.native.js.map +1 -1
- package/dist/cjs/vite/plugins/clientTreeShakePlugin.cjs +3 -1
- package/dist/cjs/vite/plugins/clientTreeShakePlugin.js +1 -1
- package/dist/cjs/vite/plugins/clientTreeShakePlugin.js.map +1 -1
- package/dist/cjs/vite/plugins/clientTreeShakePlugin.native.js +3 -1
- package/dist/cjs/vite/plugins/clientTreeShakePlugin.native.js.map +1 -1
- package/dist/cjs/vite/plugins/fileSystemRouterPlugin.cjs +98 -10
- package/dist/cjs/vite/plugins/fileSystemRouterPlugin.js +102 -13
- package/dist/cjs/vite/plugins/fileSystemRouterPlugin.js.map +2 -2
- package/dist/cjs/vite/plugins/fileSystemRouterPlugin.native.js +106 -10
- package/dist/cjs/vite/plugins/fileSystemRouterPlugin.native.js.map +1 -1
- package/dist/cjs/vite/plugins/sourceInspectorPlugin.cjs +6 -4
- package/dist/cjs/vite/plugins/sourceInspectorPlugin.js +7 -5
- package/dist/cjs/vite/plugins/sourceInspectorPlugin.js.map +1 -1
- package/dist/cjs/vite/plugins/sourceInspectorPlugin.native.js +8 -5
- package/dist/cjs/vite/plugins/sourceInspectorPlugin.native.js.map +1 -1
- package/dist/esm/cli/build.js +5 -1
- package/dist/esm/cli/build.js.map +1 -1
- package/dist/esm/cli/build.mjs +3 -1
- package/dist/esm/cli/build.mjs.map +1 -1
- package/dist/esm/cli/build.native.js +3 -1
- package/dist/esm/cli/build.native.js.map +1 -1
- package/dist/esm/cli/buildPageWorker.js +1 -1
- package/dist/esm/cli/buildPageWorker.js.map +1 -1
- package/dist/esm/cli/buildPageWorker.mjs +1 -1
- package/dist/esm/cli/buildPageWorker.mjs.map +1 -1
- package/dist/esm/cli/buildPageWorker.native.js +1 -1
- package/dist/esm/cli/buildPageWorker.native.js.map +1 -1
- package/dist/esm/daemon/picker.js +4 -0
- package/dist/esm/daemon/picker.js.map +1 -1
- package/dist/esm/daemon/picker.mjs +4 -0
- package/dist/esm/daemon/picker.mjs.map +1 -1
- package/dist/esm/daemon/picker.native.js +4 -0
- package/dist/esm/daemon/picker.native.js.map +1 -1
- package/dist/esm/daemon/tui.js +5 -1
- package/dist/esm/daemon/tui.js.map +1 -1
- package/dist/esm/daemon/tui.mjs +5 -1
- package/dist/esm/daemon/tui.mjs.map +1 -1
- package/dist/esm/daemon/tui.native.js +5 -1
- package/dist/esm/daemon/tui.native.js.map +1 -1
- package/dist/esm/router/router.js +4 -0
- package/dist/esm/router/router.js.map +1 -1
- package/dist/esm/router/router.mjs +4 -0
- package/dist/esm/router/router.mjs.map +1 -1
- package/dist/esm/router/router.native.js +4 -0
- package/dist/esm/router/router.native.js.map +1 -1
- package/dist/esm/server/oneServe.js +45 -24
- package/dist/esm/server/oneServe.js.map +1 -1
- package/dist/esm/server/oneServe.mjs +42 -25
- package/dist/esm/server/oneServe.mjs.map +1 -1
- package/dist/esm/server/oneServe.native.js +41 -24
- package/dist/esm/server/oneServe.native.js.map +1 -1
- package/dist/esm/useLoader.js +30 -12
- package/dist/esm/useLoader.js.map +1 -1
- package/dist/esm/useLoader.mjs +43 -25
- package/dist/esm/useLoader.mjs.map +1 -1
- package/dist/esm/useLoader.native.js +54 -29
- package/dist/esm/useLoader.native.js.map +1 -1
- package/dist/esm/vite/plugins/clientTreeShakePlugin.js +1 -1
- package/dist/esm/vite/plugins/clientTreeShakePlugin.js.map +1 -1
- package/dist/esm/vite/plugins/clientTreeShakePlugin.mjs +3 -1
- package/dist/esm/vite/plugins/clientTreeShakePlugin.mjs.map +1 -1
- package/dist/esm/vite/plugins/clientTreeShakePlugin.native.js +3 -1
- package/dist/esm/vite/plugins/clientTreeShakePlugin.native.js.map +1 -1
- package/dist/esm/vite/plugins/fileSystemRouterPlugin.js +102 -13
- package/dist/esm/vite/plugins/fileSystemRouterPlugin.js.map +2 -2
- package/dist/esm/vite/plugins/fileSystemRouterPlugin.mjs +98 -10
- package/dist/esm/vite/plugins/fileSystemRouterPlugin.mjs.map +1 -1
- package/dist/esm/vite/plugins/fileSystemRouterPlugin.native.js +106 -10
- package/dist/esm/vite/plugins/fileSystemRouterPlugin.native.js.map +1 -1
- package/dist/esm/vite/plugins/sourceInspectorPlugin.js +7 -5
- package/dist/esm/vite/plugins/sourceInspectorPlugin.js.map +1 -1
- package/dist/esm/vite/plugins/sourceInspectorPlugin.mjs +6 -4
- package/dist/esm/vite/plugins/sourceInspectorPlugin.mjs.map +1 -1
- package/dist/esm/vite/plugins/sourceInspectorPlugin.native.js +8 -5
- package/dist/esm/vite/plugins/sourceInspectorPlugin.native.js.map +1 -1
- package/package.json +9 -9
- package/src/cli/build.ts +7 -1
- package/src/cli/buildPageWorker.ts +4 -1
- package/src/daemon/picker.ts +8 -0
- package/src/daemon/tui.ts +8 -0
- package/src/router/router.ts +12 -0
- package/src/server/oneServe.ts +66 -35
- package/src/useLoader.ts +88 -40
- package/src/vite/plugins/clientTreeShakePlugin.ts +1 -1
- package/src/vite/plugins/fileSystemRouterPlugin.tsx +163 -22
- package/src/vite/plugins/sourceInspectorPlugin.ts +18 -16
- package/types/cli/build.d.ts.map +1 -1
- package/types/daemon/picker.d.ts.map +1 -1
- package/types/daemon/tui.d.ts.map +1 -1
- package/types/router/router.d.ts.map +1 -1
- package/types/server/oneServe.d.ts.map +1 -1
- package/types/useLoader.d.ts.map +1 -1
- package/types/vite/plugins/fileSystemRouterPlugin.d.ts.map +1 -1
- package/types/vite/plugins/sourceInspectorPlugin.d.ts.map +1 -1
package/src/server/oneServe.ts
CHANGED
|
@@ -72,6 +72,20 @@ export async function oneServe(
|
|
|
72
72
|
|
|
73
73
|
const { routeToBuildInfo, routeMap } = buildInfo as One.BuildInfo
|
|
74
74
|
|
|
75
|
+
// find nearest +not-found path by walking up from a url path
|
|
76
|
+
function findNearestNotFoundPath(urlPath: string): string {
|
|
77
|
+
let cur = urlPath
|
|
78
|
+
while (cur) {
|
|
79
|
+
const parent = cur.lastIndexOf('/') > 0 ? cur.slice(0, cur.lastIndexOf('/')) : ''
|
|
80
|
+
if (routeMap[`${parent}/+not-found`]) {
|
|
81
|
+
return `${parent}/+not-found`
|
|
82
|
+
}
|
|
83
|
+
if (!parent) break
|
|
84
|
+
cur = parent
|
|
85
|
+
}
|
|
86
|
+
return '/+not-found'
|
|
87
|
+
}
|
|
88
|
+
|
|
75
89
|
const serverOptions = {
|
|
76
90
|
...oneOptions,
|
|
77
91
|
root: '.',
|
|
@@ -450,46 +464,36 @@ url: ${url}`)
|
|
|
450
464
|
// dynamic route matched but no static HTML exists for this path
|
|
451
465
|
// (slug wasn't in generateStaticParams) - return 404
|
|
452
466
|
if (isDynamicRoute) {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
while (currentPath) {
|
|
456
|
-
const parentDir =
|
|
457
|
-
currentPath.lastIndexOf('/') > 0
|
|
458
|
-
? currentPath.slice(0, currentPath.lastIndexOf('/'))
|
|
459
|
-
: ''
|
|
460
|
-
const notFoundPath = routeMap[`${parentDir}/+not-found`]
|
|
461
|
-
|
|
462
|
-
if (notFoundPath) {
|
|
463
|
-
const fetchStaticHtml = getFetchStaticHtml()
|
|
464
|
-
let notFoundHtml: string | null = null
|
|
465
|
-
|
|
466
|
-
if (fetchStaticHtml) {
|
|
467
|
-
notFoundHtml = await fetchStaticHtml(notFoundPath)
|
|
468
|
-
}
|
|
467
|
+
const notFoundRoute = findNearestNotFoundPath(url.pathname)
|
|
468
|
+
const notFoundHtmlPath = routeMap[notFoundRoute]
|
|
469
469
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
// File not found
|
|
478
|
-
}
|
|
479
|
-
}
|
|
470
|
+
if (notFoundHtmlPath) {
|
|
471
|
+
const fetchStaticHtml = getFetchStaticHtml()
|
|
472
|
+
let notFoundHtml: string | null = null
|
|
473
|
+
|
|
474
|
+
if (fetchStaticHtml) {
|
|
475
|
+
notFoundHtml = await fetchStaticHtml(notFoundHtmlPath)
|
|
476
|
+
}
|
|
480
477
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
478
|
+
if (!notFoundHtml) {
|
|
479
|
+
try {
|
|
480
|
+
notFoundHtml = await readFile(
|
|
481
|
+
join('dist/client', notFoundHtmlPath),
|
|
482
|
+
'utf-8'
|
|
483
|
+
)
|
|
484
|
+
} catch {
|
|
485
|
+
// File not found
|
|
488
486
|
}
|
|
489
487
|
}
|
|
490
488
|
|
|
491
|
-
if (
|
|
492
|
-
|
|
489
|
+
if (notFoundHtml) {
|
|
490
|
+
const headers = new Headers()
|
|
491
|
+
headers.set('content-type', 'text/html')
|
|
492
|
+
return new Response(notFoundHtml, {
|
|
493
|
+
headers,
|
|
494
|
+
status: 404,
|
|
495
|
+
})
|
|
496
|
+
}
|
|
493
497
|
}
|
|
494
498
|
|
|
495
499
|
// no +not-found.html found, return basic 404
|
|
@@ -542,6 +546,19 @@ url: ${url}`)
|
|
|
542
546
|
// this handles all loader refetches or fetches due to navigation
|
|
543
547
|
if (url.pathname.endsWith(LOADER_JS_POSTFIX_UNCACHED)) {
|
|
544
548
|
const originalUrl = getPathFromLoaderPath(url.pathname)
|
|
549
|
+
|
|
550
|
+
// for ssg routes with dynamic params, check if this path was statically generated
|
|
551
|
+
// if not in routeMap, the slug wasn't in generateStaticParams - return 404
|
|
552
|
+
if (route.type === 'ssg' && Object.keys(route.routeKeys).length > 0) {
|
|
553
|
+
if (!routeMap[originalUrl]) {
|
|
554
|
+
const nfPath = findNearestNotFoundPath(originalUrl)
|
|
555
|
+
return new Response(
|
|
556
|
+
`export function loader(){return{__oneError:404,__oneErrorMessage:'Not Found',__oneNotFoundPath:${JSON.stringify(nfPath)}}}`,
|
|
557
|
+
{ headers: { 'Content-Type': 'text/javascript' } }
|
|
558
|
+
)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
545
562
|
const finalUrl = new URL(originalUrl, url.origin)
|
|
546
563
|
const cleanedRequest = new Request(finalUrl, request)
|
|
547
564
|
return resolveLoaderRoute(requestHandlers, cleanedRequest, finalUrl, route)
|
|
@@ -687,6 +704,20 @@ url: ${url}`)
|
|
|
687
704
|
continue
|
|
688
705
|
}
|
|
689
706
|
|
|
707
|
+
// for ssg routes with dynamic params, check if this path was statically generated
|
|
708
|
+
if (
|
|
709
|
+
route.type === 'ssg' &&
|
|
710
|
+
Object.keys(route.routeKeys).length > 0 &&
|
|
711
|
+
!routeMap[originalUrl]
|
|
712
|
+
) {
|
|
713
|
+
const nfPath = findNearestNotFoundPath(originalUrl)
|
|
714
|
+
c.header('Content-Type', 'text/javascript')
|
|
715
|
+
c.status(200)
|
|
716
|
+
return c.body(
|
|
717
|
+
`export function loader(){return{__oneError:404,__oneErrorMessage:'Not Found',__oneNotFoundPath:${JSON.stringify(nfPath)}}}`
|
|
718
|
+
)
|
|
719
|
+
}
|
|
720
|
+
|
|
690
721
|
// for now just change this
|
|
691
722
|
const loaderRoute = {
|
|
692
723
|
...route,
|
package/src/useLoader.ts
CHANGED
|
@@ -32,37 +32,38 @@ export type LoaderTimingEntry = {
|
|
|
32
32
|
const loaderTimingHistory: LoaderTimingEntry[] = []
|
|
33
33
|
const MAX_TIMING_HISTORY = 50
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
35
|
+
const recordLoaderTiming =
|
|
36
|
+
process.env.NODE_ENV === 'development'
|
|
37
|
+
? (entry: LoaderTimingEntry) => {
|
|
38
|
+
loaderTimingHistory.unshift(entry)
|
|
39
|
+
if (loaderTimingHistory.length > MAX_TIMING_HISTORY) {
|
|
40
|
+
loaderTimingHistory.pop()
|
|
41
|
+
}
|
|
42
|
+
// Dispatch event for devtools (web only - CustomEvent doesn't exist on native)
|
|
43
|
+
if (typeof window !== 'undefined' && typeof CustomEvent !== 'undefined') {
|
|
44
|
+
window.dispatchEvent(new CustomEvent('one-loader-timing', { detail: entry }))
|
|
45
|
+
|
|
46
|
+
// Also dispatch error event if there was an error
|
|
47
|
+
if (entry.error) {
|
|
48
|
+
window.dispatchEvent(
|
|
49
|
+
new CustomEvent('one-error', {
|
|
50
|
+
detail: {
|
|
51
|
+
error: {
|
|
52
|
+
message: entry.error,
|
|
53
|
+
name: 'LoaderError',
|
|
54
|
+
},
|
|
55
|
+
route: {
|
|
56
|
+
pathname: entry.path,
|
|
57
|
+
},
|
|
58
|
+
timestamp: Date.now(),
|
|
59
|
+
type: 'loader',
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
: undefined
|
|
66
67
|
|
|
67
68
|
export function getLoaderTimingHistory(): LoaderTimingEntry[] {
|
|
68
69
|
return loaderTimingHistory
|
|
@@ -146,7 +147,7 @@ export async function refetchLoader(pathname: string): Promise<void> {
|
|
|
146
147
|
|
|
147
148
|
// detect server redirect signal during refetch
|
|
148
149
|
if (result?.__oneRedirect) {
|
|
149
|
-
recordLoaderTiming({
|
|
150
|
+
recordLoaderTiming?.({
|
|
150
151
|
path: pathname,
|
|
151
152
|
startTime,
|
|
152
153
|
moduleLoadTime,
|
|
@@ -163,6 +164,21 @@ export async function refetchLoader(pathname: string): Promise<void> {
|
|
|
163
164
|
return
|
|
164
165
|
}
|
|
165
166
|
|
|
167
|
+
// detect 404 error signal during refetch
|
|
168
|
+
// don't clear data - keep existing data while navigating to not-found
|
|
169
|
+
if (result?.__oneError === 404) {
|
|
170
|
+
recordLoaderTiming?.({
|
|
171
|
+
path: pathname,
|
|
172
|
+
startTime,
|
|
173
|
+
moduleLoadTime,
|
|
174
|
+
executionTime,
|
|
175
|
+
totalTime,
|
|
176
|
+
source: 'refetch',
|
|
177
|
+
})
|
|
178
|
+
router.replace(result.__oneNotFoundPath || '/+not-found')
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
166
182
|
updateState(pathname, {
|
|
167
183
|
data: result,
|
|
168
184
|
state: 'idle',
|
|
@@ -170,7 +186,7 @@ export async function refetchLoader(pathname: string): Promise<void> {
|
|
|
170
186
|
hasLoadedOnce: true,
|
|
171
187
|
})
|
|
172
188
|
|
|
173
|
-
recordLoaderTiming({
|
|
189
|
+
recordLoaderTiming?.({
|
|
174
190
|
path: pathname,
|
|
175
191
|
startTime,
|
|
176
192
|
moduleLoadTime,
|
|
@@ -186,7 +202,7 @@ export async function refetchLoader(pathname: string): Promise<void> {
|
|
|
186
202
|
state: 'idle',
|
|
187
203
|
})
|
|
188
204
|
|
|
189
|
-
recordLoaderTiming({
|
|
205
|
+
recordLoaderTiming?.({
|
|
190
206
|
path: pathname,
|
|
191
207
|
startTime,
|
|
192
208
|
totalTime,
|
|
@@ -360,7 +376,7 @@ export function useLoaderState<
|
|
|
360
376
|
|
|
361
377
|
// detect server redirect signal on native
|
|
362
378
|
if (data?.__oneRedirect) {
|
|
363
|
-
recordLoaderTiming({
|
|
379
|
+
recordLoaderTiming?.({
|
|
364
380
|
path: currentPath,
|
|
365
381
|
startTime,
|
|
366
382
|
moduleLoadTime,
|
|
@@ -377,13 +393,29 @@ export function useLoaderState<
|
|
|
377
393
|
return
|
|
378
394
|
}
|
|
379
395
|
|
|
396
|
+
// detect 404 error signal on native
|
|
397
|
+
// don't update state so the component stays suspended while navigating
|
|
398
|
+
if (data?.__oneError === 404) {
|
|
399
|
+
recordLoaderTiming?.({
|
|
400
|
+
path: currentPath,
|
|
401
|
+
startTime,
|
|
402
|
+
moduleLoadTime,
|
|
403
|
+
executionTime,
|
|
404
|
+
totalTime,
|
|
405
|
+
source: 'initial',
|
|
406
|
+
})
|
|
407
|
+
router.replace(data.__oneNotFoundPath || '/+not-found')
|
|
408
|
+
// keep component suspended until navigation unmounts it
|
|
409
|
+
await new Promise(() => {})
|
|
410
|
+
}
|
|
411
|
+
|
|
380
412
|
updateState(currentPath, {
|
|
381
413
|
data,
|
|
382
414
|
hasLoadedOnce: true,
|
|
383
415
|
promise: undefined,
|
|
384
416
|
})
|
|
385
417
|
|
|
386
|
-
recordLoaderTiming({
|
|
418
|
+
recordLoaderTiming?.({
|
|
387
419
|
path: currentPath,
|
|
388
420
|
startTime,
|
|
389
421
|
moduleLoadTime,
|
|
@@ -398,7 +430,7 @@ export function useLoaderState<
|
|
|
398
430
|
data: {},
|
|
399
431
|
promise: undefined,
|
|
400
432
|
})
|
|
401
|
-
recordLoaderTiming({
|
|
433
|
+
recordLoaderTiming?.({
|
|
402
434
|
path: currentPath,
|
|
403
435
|
startTime,
|
|
404
436
|
totalTime,
|
|
@@ -434,7 +466,7 @@ export function useLoaderState<
|
|
|
434
466
|
|
|
435
467
|
// detect server redirect signal (fallback if preload didn't catch it)
|
|
436
468
|
if (result?.__oneRedirect) {
|
|
437
|
-
recordLoaderTiming({
|
|
469
|
+
recordLoaderTiming?.({
|
|
438
470
|
path: currentPath,
|
|
439
471
|
startTime,
|
|
440
472
|
moduleLoadTime,
|
|
@@ -451,13 +483,29 @@ export function useLoaderState<
|
|
|
451
483
|
return
|
|
452
484
|
}
|
|
453
485
|
|
|
486
|
+
// detect 404 error signal - navigate to not-found page
|
|
487
|
+
// don't update state so the component stays suspended while navigating
|
|
488
|
+
if (result?.__oneError === 404) {
|
|
489
|
+
recordLoaderTiming?.({
|
|
490
|
+
path: currentPath,
|
|
491
|
+
startTime,
|
|
492
|
+
moduleLoadTime,
|
|
493
|
+
executionTime,
|
|
494
|
+
totalTime,
|
|
495
|
+
source: 'initial',
|
|
496
|
+
})
|
|
497
|
+
router.replace(result.__oneNotFoundPath || '/+not-found')
|
|
498
|
+
// keep component suspended until navigation unmounts it
|
|
499
|
+
await new Promise(() => {})
|
|
500
|
+
}
|
|
501
|
+
|
|
454
502
|
updateState(currentPath, {
|
|
455
503
|
data: result,
|
|
456
504
|
hasLoadedOnce: true,
|
|
457
505
|
promise: undefined,
|
|
458
506
|
})
|
|
459
507
|
|
|
460
|
-
recordLoaderTiming({
|
|
508
|
+
recordLoaderTiming?.({
|
|
461
509
|
path: currentPath,
|
|
462
510
|
startTime,
|
|
463
511
|
moduleLoadTime,
|
|
@@ -473,7 +521,7 @@ export function useLoaderState<
|
|
|
473
521
|
promise: undefined,
|
|
474
522
|
})
|
|
475
523
|
|
|
476
|
-
recordLoaderTiming({
|
|
524
|
+
recordLoaderTiming?.({
|
|
477
525
|
path: currentPath,
|
|
478
526
|
startTime,
|
|
479
527
|
totalTime,
|
|
@@ -179,7 +179,7 @@ export async function transformTreeShakeClient(code: string, id: string) {
|
|
|
179
179
|
// Restore any type imports that were incorrectly removed
|
|
180
180
|
restoreTypeImports(ast, typeImports)
|
|
181
181
|
|
|
182
|
-
const out = generate(ast)
|
|
182
|
+
const out = generate(ast, { retainLines: true })
|
|
183
183
|
|
|
184
184
|
// add back in empty or filled loader and genparams
|
|
185
185
|
const codeOut =
|
|
@@ -50,6 +50,34 @@ export function createFileSystemRouterPlugin(options: One.PluginOptions): Plugin
|
|
|
50
50
|
|
|
51
51
|
function createRequestHandler() {
|
|
52
52
|
const routerRoot = getRouterRootFromOneOptions(options)
|
|
53
|
+
|
|
54
|
+
// find the nearest +not-found route by walking up from a route's directory
|
|
55
|
+
async function findNearestNotFoundPath(routeFile: string): Promise<string> {
|
|
56
|
+
const routeDir = routeFile.replace(/\/[^/]+$/, '')
|
|
57
|
+
let searchDir = routeDir
|
|
58
|
+
while (true) {
|
|
59
|
+
for (const ext of ['.tsx', '.ts', '.jsx', '.js']) {
|
|
60
|
+
const candidate = path.join(routerRoot, searchDir, `+not-found${ext}`)
|
|
61
|
+
try {
|
|
62
|
+
const mod = await runner.import(candidate)
|
|
63
|
+
if (mod?.default) {
|
|
64
|
+
return searchDir ? `/${searchDir}/+not-found` : '/+not-found'
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// not found at this level
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (!searchDir) break
|
|
71
|
+
const parent = searchDir.replace(/\/[^/]+$/, '')
|
|
72
|
+
if (parent === searchDir) {
|
|
73
|
+
searchDir = ''
|
|
74
|
+
} else {
|
|
75
|
+
searchDir = parent
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return '/+not-found'
|
|
79
|
+
}
|
|
80
|
+
|
|
53
81
|
return createHandleRequest(
|
|
54
82
|
{
|
|
55
83
|
async handlePage({ route, url, loaderProps }) {
|
|
@@ -214,7 +242,90 @@ export function createFileSystemRouterPlugin(options: One.PluginOptions): Plugin
|
|
|
214
242
|
|
|
215
243
|
LoaderDataCache[route.file] = loaderData
|
|
216
244
|
|
|
217
|
-
|
|
245
|
+
// detect 404: not-found routes, missing page exports, or ssg dynamic routes with invalid slugs
|
|
246
|
+
const isDynamicRoute = Object.keys(route.routeKeys || {}).length > 0
|
|
247
|
+
|
|
248
|
+
// for ssg dynamic routes, check generateStaticParams to validate the slug
|
|
249
|
+
let isMissingSsgSlug = false
|
|
250
|
+
if (route.type === 'ssg' && isDynamicRoute && exported.generateStaticParams) {
|
|
251
|
+
const staticParams = await exported.generateStaticParams({
|
|
252
|
+
params: loaderProps?.params,
|
|
253
|
+
})
|
|
254
|
+
const currentParams = loaderProps?.params || {}
|
|
255
|
+
isMissingSsgSlug = !staticParams.some((sp: Record<string, string>) =>
|
|
256
|
+
Object.keys(sp).every((key) => sp[key] === currentParams[key])
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const is404 =
|
|
261
|
+
route.isNotFound ||
|
|
262
|
+
!getPageExport(exported) ||
|
|
263
|
+
isMissingSsgSlug ||
|
|
264
|
+
(route.type === 'ssg' && isDynamicRoute && loaderData === undefined)
|
|
265
|
+
|
|
266
|
+
// for ssg dynamic routes with invalid slug, render the not-found page instead
|
|
267
|
+
if (
|
|
268
|
+
isMissingSsgSlug ||
|
|
269
|
+
(route.type === 'ssg' && isDynamicRoute && loaderData === undefined)
|
|
270
|
+
) {
|
|
271
|
+
// find nearest +not-found by walking up the route's directory
|
|
272
|
+
let notFoundExported: any = {}
|
|
273
|
+
let notFoundRoutePath = '/+not-found'
|
|
274
|
+
const routeDir = route.file.replace(/\/[^/]+$/, '')
|
|
275
|
+
let searchDir = routeDir
|
|
276
|
+
while (true) {
|
|
277
|
+
for (const ext of ['.tsx', '.ts', '.jsx', '.js']) {
|
|
278
|
+
const candidate = path.join(routerRoot, searchDir, `+not-found${ext}`)
|
|
279
|
+
try {
|
|
280
|
+
notFoundExported = await runner.import(candidate)
|
|
281
|
+
if (notFoundExported?.default) {
|
|
282
|
+
notFoundRoutePath = searchDir
|
|
283
|
+
? `/${searchDir}/+not-found`
|
|
284
|
+
: '/+not-found'
|
|
285
|
+
break
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
// not found at this level
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (notFoundExported?.default || !searchDir) break
|
|
292
|
+
const parent = searchDir.replace(/\/[^/]+$/, '')
|
|
293
|
+
if (parent === searchDir) {
|
|
294
|
+
searchDir = ''
|
|
295
|
+
} else {
|
|
296
|
+
searchDir = parent
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (notFoundExported.default) {
|
|
301
|
+
// override the route to render the not-found page
|
|
302
|
+
setServerContext({
|
|
303
|
+
loaderData: undefined,
|
|
304
|
+
loaderProps,
|
|
305
|
+
matches: [],
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
const notFoundHtml = await render({
|
|
309
|
+
mode: 'ssg',
|
|
310
|
+
loaderData: undefined,
|
|
311
|
+
loaderProps,
|
|
312
|
+
path: notFoundRoutePath,
|
|
313
|
+
preloads,
|
|
314
|
+
matches: [],
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
return new Response(notFoundHtml, {
|
|
318
|
+
status: 404,
|
|
319
|
+
headers: { 'Content-Type': 'text/html' },
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// fallback if no +not-found page exists
|
|
324
|
+
return new Response('<html><body><h1>404 - Not Found</h1></body></html>', {
|
|
325
|
+
status: 404,
|
|
326
|
+
headers: { 'Content-Type': 'text/html' },
|
|
327
|
+
})
|
|
328
|
+
}
|
|
218
329
|
|
|
219
330
|
const html = await render({
|
|
220
331
|
mode: isSpaShell
|
|
@@ -298,34 +409,64 @@ export function createFileSystemRouterPlugin(options: One.PluginOptions): Plugin
|
|
|
298
409
|
|
|
299
410
|
const exported = await runner.import(routeFile)
|
|
300
411
|
|
|
412
|
+
// for ssg dynamic routes, check generateStaticParams to validate the slug
|
|
413
|
+
const isDynamicRoute = Object.keys(route.routeKeys || {}).length > 0
|
|
414
|
+
if (route.type === 'ssg' && isDynamicRoute && exported.generateStaticParams) {
|
|
415
|
+
const staticParams = await exported.generateStaticParams({
|
|
416
|
+
params: loaderProps?.params,
|
|
417
|
+
})
|
|
418
|
+
const currentParams = loaderProps?.params || {}
|
|
419
|
+
const isValidSlug = staticParams.some((sp: Record<string, string>) =>
|
|
420
|
+
Object.keys(sp).every((key) => sp[key] === currentParams[key])
|
|
421
|
+
)
|
|
422
|
+
if (!isValidSlug) {
|
|
423
|
+
const nfPath = await findNearestNotFoundPath(route.file)
|
|
424
|
+
return `export function loader(){return{__oneError:404,__oneErrorMessage:'Not Found',__oneNotFoundPath:${JSON.stringify(nfPath)}}}`
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
301
428
|
// Track file dependencies from loader for hot reload
|
|
302
429
|
let loaderData: any
|
|
303
430
|
if (exported.loader) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
431
|
+
try {
|
|
432
|
+
const tracked = await trackLoaderDependencies(() =>
|
|
433
|
+
exported.loader(loaderProps)
|
|
434
|
+
)
|
|
435
|
+
loaderData = tracked.result
|
|
308
436
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
437
|
+
// if the loader returned a Response (e.g. redirect()), throw it
|
|
438
|
+
// so it bubbles up through resolveResponse and can be transformed
|
|
439
|
+
// into a JS redirect module for client-side navigation
|
|
440
|
+
if (isResponse(loaderData)) {
|
|
441
|
+
throw loaderData
|
|
442
|
+
}
|
|
315
443
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
444
|
+
// Register dependencies: map file path -> route paths that depend on it
|
|
445
|
+
const routePath = loaderProps?.path || '/'
|
|
446
|
+
for (const dep of tracked.dependencies) {
|
|
447
|
+
// Resolve to absolute path for consistent lookup when file changes
|
|
448
|
+
const absoluteDep = path.resolve(dep)
|
|
449
|
+
if (!loaderFileDependencies.has(absoluteDep)) {
|
|
450
|
+
loaderFileDependencies.set(absoluteDep, new Set())
|
|
451
|
+
server?.watcher.add(absoluteDep)
|
|
452
|
+
if (debugLoaderDeps) {
|
|
453
|
+
console.info(` ⓵ [loader-dep] watching: ${absoluteDep}`)
|
|
454
|
+
}
|
|
326
455
|
}
|
|
456
|
+
loaderFileDependencies.get(absoluteDep)!.add(routePath)
|
|
457
|
+
}
|
|
458
|
+
} catch (err) {
|
|
459
|
+
// re-throw Response errors (redirects)
|
|
460
|
+
if (isResponse(err)) {
|
|
461
|
+
throw err
|
|
462
|
+
}
|
|
463
|
+
// for file-not-found errors (e.g., missing MDX for non-existent slug),
|
|
464
|
+
// return a 404 signal so the client navigates to +not-found
|
|
465
|
+
if ((err as any)?.code === 'ENOENT') {
|
|
466
|
+
const nfPath = await findNearestNotFoundPath(route.file)
|
|
467
|
+
return `export function loader(){return{__oneError:404,__oneErrorMessage:'Not Found',__oneNotFoundPath:${JSON.stringify(nfPath)}}}`
|
|
327
468
|
}
|
|
328
|
-
|
|
469
|
+
throw err
|
|
329
470
|
}
|
|
330
471
|
}
|
|
331
472
|
|
|
@@ -15,7 +15,6 @@ interface JsxLocation {
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Parse code with oxc and find all JSX opening elements.
|
|
18
|
-
* Returns insertion points sorted by offset (descending for safe insertion).
|
|
19
18
|
*/
|
|
20
19
|
async function findJsxElements(code: string, filename: string): Promise<JsxLocation[]> {
|
|
21
20
|
const result = await parse(filename, code)
|
|
@@ -53,14 +52,12 @@ async function findJsxElements(code: string, filename: string): Promise<JsxLocat
|
|
|
53
52
|
|
|
54
53
|
// skip Fragment and already-tagged elements
|
|
55
54
|
if (tagName && tagName !== 'Fragment' && !tagName.endsWith('.Fragment')) {
|
|
56
|
-
// Check if already has data-one-source
|
|
57
55
|
const hasSourceAttr = node.attributes?.some(
|
|
58
56
|
(attr: any) =>
|
|
59
57
|
attr.type === 'JSXAttribute' && attr.name?.name === 'data-one-source'
|
|
60
58
|
)
|
|
61
59
|
|
|
62
60
|
if (!hasSourceAttr) {
|
|
63
|
-
// Insert position is right after the tag name
|
|
64
61
|
const nameEnd = node.name.end
|
|
65
62
|
const loc = getLocation(node.start)
|
|
66
63
|
|
|
@@ -73,7 +70,6 @@ async function findJsxElements(code: string, filename: string): Promise<JsxLocat
|
|
|
73
70
|
}
|
|
74
71
|
}
|
|
75
72
|
|
|
76
|
-
// Walk all child nodes in consistent order
|
|
77
73
|
for (const key of Object.keys(node)) {
|
|
78
74
|
if (key === 'parent') continue
|
|
79
75
|
const value = node[key]
|
|
@@ -89,19 +85,15 @@ async function findJsxElements(code: string, filename: string): Promise<JsxLocat
|
|
|
89
85
|
|
|
90
86
|
walk(result.program)
|
|
91
87
|
|
|
92
|
-
// Sort by offset descending so we can insert from end to start without shifting positions
|
|
93
88
|
return locations.sort((a, b) => b.insertOffset - a.insertOffset)
|
|
94
89
|
}
|
|
95
90
|
|
|
91
|
+
type TransformOut = { code: string; map?: null } | undefined
|
|
92
|
+
|
|
96
93
|
/**
|
|
97
94
|
* Transforms JSX to inject data-one-source attributes using oxc-parser.
|
|
98
|
-
* Embeds line:column directly in the attribute - safe because both SSR and client
|
|
99
|
-
* run this same transform with enforce:'pre' on the same source.
|
|
100
95
|
*/
|
|
101
|
-
async function injectSourceToJsx(
|
|
102
|
-
code: string,
|
|
103
|
-
id: string
|
|
104
|
-
): Promise<{ code: string; map?: null } | undefined> {
|
|
96
|
+
async function injectSourceToJsx(code: string, id: string): Promise<TransformOut> {
|
|
105
97
|
const [filePath] = id.split('?')
|
|
106
98
|
if (!filePath) return
|
|
107
99
|
|
|
@@ -155,9 +147,10 @@ async function openInEditor(
|
|
|
155
147
|
|
|
156
148
|
// track connected vscode clients and browser clients
|
|
157
149
|
const vscodeClients = new Set<WebSocket>()
|
|
158
|
-
let viteServer: ViteDevServer | null = null
|
|
159
150
|
|
|
160
151
|
export function sourceInspectorPlugin(): Plugin[] {
|
|
152
|
+
const cache = new Map<string, TransformOut>()
|
|
153
|
+
|
|
161
154
|
return [
|
|
162
155
|
// Transform plugin - injects data-one-source attributes
|
|
163
156
|
{
|
|
@@ -165,7 +158,7 @@ export function sourceInspectorPlugin(): Plugin[] {
|
|
|
165
158
|
enforce: 'pre',
|
|
166
159
|
apply: 'serve',
|
|
167
160
|
|
|
168
|
-
transform(code, id) {
|
|
161
|
+
async transform(code, id) {
|
|
169
162
|
const envName = this.environment?.name
|
|
170
163
|
// Skip native environments only - transform both client and SSR for consistency
|
|
171
164
|
if (envName === 'ios' || envName === 'android') return
|
|
@@ -181,7 +174,18 @@ export function sourceInspectorPlugin(): Plugin[] {
|
|
|
181
174
|
|
|
182
175
|
if (!id.endsWith('.jsx') && !id.endsWith('.tsx')) return
|
|
183
176
|
|
|
184
|
-
|
|
177
|
+
if (cache.has(code)) {
|
|
178
|
+
return cache.get(code)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const out = await injectSourceToJsx(code, id)
|
|
182
|
+
cache.set(code, out)
|
|
183
|
+
|
|
184
|
+
if (cache.size > 100) {
|
|
185
|
+
cache.clear()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return out
|
|
185
189
|
},
|
|
186
190
|
},
|
|
187
191
|
|
|
@@ -193,8 +197,6 @@ export function sourceInspectorPlugin(): Plugin[] {
|
|
|
193
197
|
apply: 'serve',
|
|
194
198
|
|
|
195
199
|
configureServer(server: ViteDevServer) {
|
|
196
|
-
viteServer = server
|
|
197
|
-
|
|
198
200
|
// set up websocket server for vscode cursor position
|
|
199
201
|
let wss: InstanceType<typeof import('ws').WebSocketServer> | null = null
|
|
200
202
|
|
package/types/cli/build.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/cli/build.ts"],"names":[],"mappings":"AAwDA,wBAAsB,KAAK,CAAC,IAAI,EAAE;IAChC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,SAAS,CAAA;IACpC,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/cli/build.ts"],"names":[],"mappings":"AAwDA,wBAAsB,KAAK,CAAC,IAAI,EAAE;IAChC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,SAAS,CAAA;IACpC,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,iBAygCA"}
|