@weft-js/start 0.1.3 → 0.1.5-nightly.20261111.3
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/README.md +1 -1
- package/package.json +3 -3
- package/src/app.mjs +1 -1
- package/src/boot.ts +2 -0
- package/src/capture.ts +3 -1
- package/src/contract.mjs +1 -1
- package/src/inspect.mjs +1 -1
- package/src/run/admission.ts +10 -23
- package/src/run/listeners.ts +12 -1
- package/src/run/navigation.ts +4 -1
- package/src/run/prefetch.ts +2 -1
package/README.md
CHANGED
|
@@ -101,7 +101,7 @@ pnpm add @weft-js/start @weft-js/core
|
|
|
101
101
|
bun add @weft-js/start @weft-js/core
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
Start needs Node
|
|
104
|
+
Start needs Node 24 or later (the `engines` field), Bun or Deno. Drizzle, Better Auth, Zod and the database drivers are
|
|
105
105
|
optional peer dependencies: install the ones your app uses, or let `weft add db` and `weft add auth` do it.
|
|
106
106
|
|
|
107
107
|
## Quick start
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weft-js/start",
|
|
3
|
-
"version": "0.1.3",
|
|
3
|
+
"version": "0.1.5-nightly.20261111.3",
|
|
4
4
|
"description": "weft Start: file routes, server functions, streaming SSR, a database and auth",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"weft"
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"vite": "8.3.5"
|
|
79
79
|
},
|
|
80
80
|
"peerDependencies": {
|
|
81
|
-
"@weft-js/core": "^0.1.3",
|
|
81
|
+
"@weft-js/core": "^0.1.5-nightly.20261111.3",
|
|
82
82
|
"better-auth": "^1.7.0",
|
|
83
83
|
"drizzle-orm": "^0.45.0",
|
|
84
84
|
"drizzle-zod": "^0.8.0",
|
|
@@ -113,6 +113,6 @@
|
|
|
113
113
|
}
|
|
114
114
|
},
|
|
115
115
|
"engines": {
|
|
116
|
-
"node": ">=
|
|
116
|
+
"node": ">=24"
|
|
117
117
|
}
|
|
118
118
|
}
|
package/src/app.mjs
CHANGED
|
@@ -49,7 +49,7 @@ function config({
|
|
|
49
49
|
define: { __WEFT_SOCKETS__: JSON.stringify(holdsSockets(preset)) },
|
|
50
50
|
}),
|
|
51
51
|
plugins: [
|
|
52
|
-
weft({ hydratable: true }),
|
|
52
|
+
weft({ hydratable: true, csp: site?.csp }),
|
|
53
53
|
weftStart({ root, site, hydration }),
|
|
54
54
|
runner && devSockets(),
|
|
55
55
|
nitro({
|
package/src/boot.ts
CHANGED
package/src/capture.ts
CHANGED
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
//
|
|
7
7
|
// q the events n the page needs its code at once w wake: (element, lead), a lead being only a hint of intent
|
|
8
8
|
// p replay one r replaying, so don't capture d the visitor asked for less data l the last lead
|
|
9
|
+
// s the page is running: until then a page that hydrates whole (`n`) holds a click, submit or reset anywhere, not just
|
|
10
|
+
// on an island, since nothing is listening yet. A plain link is left to the browser: it works without code.
|
|
9
11
|
//
|
|
10
12
|
// Written by hand and small, since every such page pays it: `boot.ts` and weft's `island.ts` are the other two halves.
|
|
11
13
|
|
|
12
|
-
const CODE = `(n=>{var d=document,c=navigator.connection,q=window.__wq={q:[],n:n,w(){},d:matchMedia("(prefers-reduced-data: reduce)").matches||!!(c&&c.saveData),p(e){var t=e.target,r=q.r,s=e.submitter;if(t.isConnected){q.r=1;try{e.type=="submit"?t.requestSubmit(s&&s.isConnected?s:void 0):e.type=="reset"?t.reset():t.dispatchEvent(new e.constructor(e.type,e))}finally{q.r=r}}}},o=t=>t.closest&&t.closest("[wk]"),h=e=>{var k=!q.r&&o(e.
|
|
14
|
+
const CODE = `(n=>{var d=document,c=navigator.connection,q=window.__wq={q:[],n:n,w(){},d:matchMedia("(prefers-reduced-data: reduce)").matches||!!(c&&c.saveData),p(e){var t=e.target,r=q.r,s=e.submitter;if(t.isConnected){q.r=1;try{e.type=="submit"?t.requestSubmit(s&&s.isConnected?s:void 0):e.type=="reset"?t.reset():t.dispatchEvent(new e.constructor(e.type,e))}finally{q.r=r}}}},o=t=>t.closest&&t.closest("[wk]"),h=e=>{var t=e.target,k=!q.r&&(o(t)||q.n&&!q.s&&/^(click|submit|reset)$/.test(e.type)&&t.closest&&!t.closest("a[href]")&&d.documentElement);if(k&&(!k.$k||q.q.length)&&!((e.metaKey||e.ctrlKey||e.shiftKey)&&e.type=="click"&&e.target.closest("a"))){q.q.push(e);/^(click|submit|reset)$/.test(e.type)&&e.preventDefault();q.w(k)}},l=e=>{var k=o(e.target);k&&!k.$k&&!q.d&&q.w(k,1)};"click dblclick pointerdown pointerup mousedown mouseup keydown keyup input change submit reset focus blur focusin focusout".split(" ").forEach(t=>d.addEventListener(t,h,!0));"pointerover touchstart".split(" ").forEach(t=>d.addEventListener(t,l,{capture:!0,passive:!0}))})`
|
|
13
15
|
|
|
14
16
|
/** The script's text. `now`: the page has work that can't wait, so its code loads straight away. */
|
|
15
17
|
export const capture = (now: boolean) => `${CODE}(${now ? 1 : 0})`
|
package/src/contract.mjs
CHANGED
|
@@ -233,7 +233,7 @@ async function withFns({ root, site = {} }, fn) {
|
|
|
233
233
|
logLevel: 'silent',
|
|
234
234
|
appType: 'custom',
|
|
235
235
|
server: { middlewareMode: true, hmr: false, ws: false },
|
|
236
|
-
plugins: [weft({ hydratable: true }), weftStart({ root, site })],
|
|
236
|
+
plugins: [weft({ hydratable: true, csp: site?.csp }), weftStart({ root, site })],
|
|
237
237
|
})
|
|
238
238
|
await server.ssrLoadModule('virtual:weft/fns')
|
|
239
239
|
const { fns } = await server.ssrLoadModule(join(startDir, 'data.ts'))
|
package/src/inspect.mjs
CHANGED
|
@@ -136,7 +136,7 @@ export async function routesMeta({ root, site = {}, preset }) {
|
|
|
136
136
|
logLevel: 'silent',
|
|
137
137
|
appType: 'custom',
|
|
138
138
|
server: { middlewareMode: true, hmr: false, ws: false },
|
|
139
|
-
plugins: [weft({ hydratable: true }), weftStart({ root, site })],
|
|
139
|
+
plugins: [weft({ hydratable: true, csp: site?.csp }), weftStart({ root, site })],
|
|
140
140
|
})
|
|
141
141
|
try {
|
|
142
142
|
const { manifest, modules } = await server.ssrLoadModule('virtual:weft/routes')
|
package/src/run/admission.ts
CHANGED
|
@@ -35,46 +35,33 @@ export function admitLink(event: Event, routes: ClientRoutes) {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Links go on their click. A press (pointerdown) only warms the route: it starts the fetch the click will claim, so the
|
|
40
|
+
* head start is kept, but the page never changes under a pointer that is still down. Swapping it on the press lost the
|
|
41
|
+
* click itself (the button came up on the new page, so the click landed on <html> or never fired), and with it every
|
|
42
|
+
* click handler: a menu that closes when its link is followed, a sheet, an analytics hook. The returned disposer
|
|
43
|
+
* removes only these listeners.
|
|
44
|
+
*/
|
|
39
45
|
export function routeClicks(
|
|
40
46
|
doc: Document,
|
|
41
47
|
routes: ClientRoutes,
|
|
42
48
|
go: (href: string) => void | Promise<void>,
|
|
49
|
+
warm?: (url: URL) => void,
|
|
43
50
|
) {
|
|
44
|
-
let accepted: { id: number; anchor: HTMLAnchorElement } | undefined
|
|
45
51
|
const down = (event: PointerEvent) => {
|
|
46
|
-
accepted = undefined
|
|
47
52
|
const target = admitLink(event, routes)
|
|
48
|
-
if (!target)
|
|
49
|
-
accepted = { id: event.pointerId, anchor: target.anchor }
|
|
50
|
-
if (!target.noop) void Promise.resolve(go(target.url.href)).catch(() => {})
|
|
51
|
-
}
|
|
52
|
-
const cancel = (event: PointerEvent) => {
|
|
53
|
-
if (accepted?.id === event.pointerId) accepted = undefined
|
|
53
|
+
if (target && !target.noop) warm?.(target.url)
|
|
54
54
|
}
|
|
55
55
|
const click = (event: MouseEvent) => {
|
|
56
|
-
const record = accepted
|
|
57
|
-
accepted = undefined
|
|
58
|
-
const pointer = event as PointerEvent
|
|
59
|
-
if (
|
|
60
|
-
record &&
|
|
61
|
-
event.detail > 0 &&
|
|
62
|
-
(pointer.pointerId === record.id || event.composedPath().includes(record.anchor))
|
|
63
|
-
) {
|
|
64
|
-
event.preventDefault()
|
|
65
|
-
return
|
|
66
|
-
}
|
|
67
56
|
const target = admitLink(event, routes)
|
|
68
57
|
if (!target) return
|
|
69
58
|
event.preventDefault()
|
|
70
|
-
if (!target.noop) void go(target.url.href)
|
|
59
|
+
if (!target.noop) void Promise.resolve(go(target.url.href)).catch(() => {})
|
|
71
60
|
}
|
|
72
61
|
doc.addEventListener('pointerdown', down)
|
|
73
|
-
doc.addEventListener('pointercancel', cancel)
|
|
74
62
|
doc.addEventListener('click', click)
|
|
75
63
|
return () => {
|
|
76
64
|
doc.removeEventListener('pointerdown', down)
|
|
77
|
-
doc.removeEventListener('pointercancel', cancel)
|
|
78
65
|
doc.removeEventListener('click', click)
|
|
79
66
|
}
|
|
80
67
|
}
|
package/src/run/listeners.ts
CHANGED
|
@@ -30,6 +30,7 @@ export function listeners(nav: Navigation, hot: () => Hot | undefined) {
|
|
|
30
30
|
const q = (window as Window & { __wq?: Held }).__wq
|
|
31
31
|
if (q) {
|
|
32
32
|
q.w = wake
|
|
33
|
+
q.s = 1
|
|
33
34
|
if (q.l) wake(q.l)
|
|
34
35
|
drain()
|
|
35
36
|
}
|
|
@@ -37,7 +38,17 @@ export function listeners(nav: Navigation, hot: () => Hot | undefined) {
|
|
|
37
38
|
|
|
38
39
|
// A fade can intercept a click; replay it on the element underneath once the picture is gone.
|
|
39
40
|
function clicks(nav: Navigation) {
|
|
40
|
-
|
|
41
|
+
const network = (
|
|
42
|
+
navigator as Navigator & { connection?: { saveData?: boolean; effectiveType?: string } }
|
|
43
|
+
).connection
|
|
44
|
+
routeClicks(
|
|
45
|
+
document,
|
|
46
|
+
nav.routes,
|
|
47
|
+
(href) => nav.go(href),
|
|
48
|
+
(url) => {
|
|
49
|
+
if (!document.hidden) nav.prefetch.start(url, 'press', network, navigator.onLine)
|
|
50
|
+
},
|
|
51
|
+
)
|
|
41
52
|
let ended: Promise<unknown> | undefined
|
|
42
53
|
document.addEventListener(
|
|
43
54
|
'pointerdown',
|
package/src/run/navigation.ts
CHANGED
|
@@ -123,7 +123,10 @@ export class Navigation {
|
|
|
123
123
|
if (!current()) return
|
|
124
124
|
if (scroll) {
|
|
125
125
|
const target = url.hash && document.getElementById(decodeURIComponent(url.hash.slice(1)))
|
|
126
|
-
|
|
126
|
+
// a new page arrives where it starts, at once: warp makes the root scroll smooth for in-page anchors, and a
|
|
127
|
+
// route change that glided down from the old page's offset read as the old page scrolling away
|
|
128
|
+
if (target) target.scrollIntoView({ behavior: 'instant' })
|
|
129
|
+
else window.scrollTo({ top: 0, left: 0, behavior: 'instant' })
|
|
127
130
|
}
|
|
128
131
|
}
|
|
129
132
|
}
|
package/src/run/prefetch.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { RouteCache, Read } from './cache'
|
|
2
2
|
import { ask } from './helpers'
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/** hover is a guess; focus and a press (the button is down, the click is coming) are near certain */
|
|
5
|
+
type Intent = 'hover' | 'focus' | 'press'
|
|
5
6
|
type Network = { saveData?: boolean; effectiveType?: string }
|
|
6
7
|
type Job = { ctrl: AbortController; promise: Promise<Read | undefined>; promoted: boolean }
|
|
7
8
|
|