@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 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 26 or later (the `engines` field), Bun or Deno. Drizzle, Better Auth, Zod and the database drivers are
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": ">=26"
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
@@ -8,6 +8,8 @@ export interface Held {
8
8
  p: (held: unknown) => void
9
9
  w?: (k: Element, lead?: 1) => void
10
10
  r?: 1
11
+ /** The page is running: a page that hydrates whole stops holding events off its islands. */
12
+ s?: 1
11
13
  n?: unknown
12
14
  l?: Element
13
15
  d?: unknown
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.target);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}))})`
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')
@@ -35,46 +35,33 @@ export function admitLink(event: Event, routes: ClientRoutes) {
35
35
  }
36
36
  }
37
37
 
38
- /** Pointer admission and its click fallback; the returned disposer removes only these listeners. */
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) return
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
  }
@@ -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
- routeClicks(document, nav.routes, (href) => nav.go(href))
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',
@@ -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
- target ? target.scrollIntoView() : window.scrollTo(0, 0)
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
  }
@@ -1,7 +1,8 @@
1
1
  import type { RouteCache, Read } from './cache'
2
2
  import { ask } from './helpers'
3
3
 
4
- type Intent = 'hover' | 'focus'
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