@weft-js/start 0.0.0-stage → 0.1.1
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 +16 -0
- package/README.md +41 -2
- package/package.json +107 -4
- package/src/app.mjs +93 -0
- package/src/auth/better-auth.ts +58 -0
- package/src/auth/drizzle.ts +17 -0
- package/src/auth.ts +47 -0
- package/src/boot.ts +44 -0
- package/src/builtin.tsx +19 -0
- package/src/bus/postgres.ts +91 -0
- package/src/bus.ts +48 -0
- package/src/capture.d.mts +1 -0
- package/src/capture.mjs +15 -0
- package/src/changes-sql.d.mts +14 -0
- package/src/changes-sql.mjs +99 -0
- package/src/changes.ts +253 -0
- package/src/channel-fake.ts +55 -0
- package/src/channel.ts +622 -0
- package/src/client.tsx +20 -0
- package/src/contract.mjs +312 -0
- package/src/data.ts +929 -0
- package/src/db/bun-sqlite.d.ts +10 -0
- package/src/db/changes-mysql.ts +86 -0
- package/src/db/changes-postgres.ts +159 -0
- package/src/db/changes-sqlite.ts +116 -0
- package/src/db/core.ts +63 -0
- package/src/db/index.ts +27 -0
- package/src/db/mysql.ts +67 -0
- package/src/db/postgres.ts +65 -0
- package/src/db/sqlite.ts +139 -0
- package/src/derive.d.mts +16 -0
- package/src/derive.mjs +140 -0
- package/src/dev-runtime.mjs +42 -0
- package/src/exec.ts +519 -0
- package/src/extension.ts +38 -0
- package/src/first-paint.ts +57 -0
- package/src/form.tsx +134 -0
- package/src/gate.css +6 -0
- package/src/hub.ts +822 -0
- package/src/index.ts +106 -0
- package/src/inline.mjs +2 -0
- package/src/inspect.mjs +280 -0
- package/src/link.ts +461 -0
- package/src/list-drizzle.ts +50 -0
- package/src/list.ts +224 -0
- package/src/listen.ts +28 -0
- package/src/live-fake.ts +42 -0
- package/src/live-target.mjs +147 -0
- package/src/live.ts +499 -0
- package/src/media.ts +43 -0
- package/src/migrations.mjs +19 -0
- package/src/mux-session.ts +6 -0
- package/src/mux.ts +135 -0
- package/src/og/DefaultCard.tsx +60 -0
- package/src/og/fonts.mjs +64 -0
- package/src/og/images.mjs +54 -0
- package/src/og/index.mjs +146 -0
- package/src/og/native.mjs +29 -0
- package/src/range.mjs +19 -0
- package/src/router.ts +149 -0
- package/src/routes.mjs +142 -0
- package/src/run.tsx +202 -0
- package/src/rust.ts +333 -0
- package/src/server.tsx +1021 -0
- package/src/shape.d.mts +3 -0
- package/src/shape.mjs +73 -0
- package/src/share-server.ts +208 -0
- package/src/share.ts +503 -0
- package/src/sheets.mjs +75 -0
- package/src/sidecar.mjs +503 -0
- package/src/socket.ts +389 -0
- package/src/stream.ts +13 -0
- package/src/verify.ts +251 -0
- package/src/view.tsx +47 -0
- package/src/vite.mjs +899 -0
package/src/db/sqlite.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// SQLite for `@weft-js/start` (research 28 §1): Node's and Deno's `node:sqlite` or Bun's `bun:sqlite` behind Drizzle's
|
|
2
|
+
// proxy driver, so no native binary and the same async API as Postgres. One connection, opened on the first statement
|
|
3
|
+
// (`weft build` loads this module and must not touch a file). A transaction's body is async, so it holds a gate:
|
|
4
|
+
// statements outside it wait, or a rollback would take another request's writes with it.
|
|
5
|
+
|
|
6
|
+
// oxlint-disable-next-line typescript/triple-slash-reference -- an installed app's tsc doesn't include this file by itself, and it declares a module, so nothing can import it
|
|
7
|
+
/// <reference path="./bun-sqlite.d.ts" />
|
|
8
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
9
|
+
import { mkdirSync } from 'node:fs'
|
|
10
|
+
import { dirname } from 'node:path'
|
|
11
|
+
import type { SQLInputValue } from 'node:sqlite'
|
|
12
|
+
import { drizzle, type SqliteRemoteDatabase } from 'drizzle-orm/sqlite-proxy'
|
|
13
|
+
import { StartError } from '../data'
|
|
14
|
+
import { gate, register, statements, touch, type DbHandle, type Migration } from './core'
|
|
15
|
+
|
|
16
|
+
/** How Drizzle's proxy driver asks for rows. */
|
|
17
|
+
export type Method = 'run' | 'all' | 'values' | 'get'
|
|
18
|
+
|
|
19
|
+
interface Conn {
|
|
20
|
+
/** Array rows, as the proxy driver wants them. */
|
|
21
|
+
query(sql: string, params: unknown[], method: Method): unknown
|
|
22
|
+
exec(sql: string): void
|
|
23
|
+
close?(): void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The file open on this runtime's driver: Bun's, or `node:sqlite`. */
|
|
27
|
+
export async function open(path: string): Promise<Conn> {
|
|
28
|
+
if (path !== ':memory:') mkdirSync(dirname(path), { recursive: true })
|
|
29
|
+
const pick = (rows: unknown[][], method: Method) => (method === 'get' ? rows[0] : rows)
|
|
30
|
+
let conn: Conn
|
|
31
|
+
if ('Bun' in globalThis) {
|
|
32
|
+
const { Database } = await import('bun:sqlite')
|
|
33
|
+
const db = new Database(path, { create: true })
|
|
34
|
+
conn = {
|
|
35
|
+
query: (sql, params, method) => (method === 'run' ? void db.prepare(sql).run(...params) : pick(db.prepare(sql).values(...params), method)),
|
|
36
|
+
exec: sql => void db.run(sql),
|
|
37
|
+
close: () => db.close(),
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
const { DatabaseSync } = await import('node:sqlite')
|
|
41
|
+
const db = new DatabaseSync(path)
|
|
42
|
+
conn = {
|
|
43
|
+
query: (sql, params, method) => {
|
|
44
|
+
const s = db.prepare(sql)
|
|
45
|
+
if (method === 'run') return void s.run(...(params as SQLInputValue[]))
|
|
46
|
+
s.setReturnArrays(true)
|
|
47
|
+
return pick(s.all(...(params as SQLInputValue[])) as unknown as unknown[][], method)
|
|
48
|
+
},
|
|
49
|
+
exec: sql => db.exec(sql),
|
|
50
|
+
close: () => db.close(),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (path !== ':memory:') conn.exec('pragma journal_mode = wal')
|
|
54
|
+
conn.exec('pragma foreign_keys = on; pragma busy_timeout = 5000')
|
|
55
|
+
return conn
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** `createDb` on SQLite: the schema, the file, and whether to migrate on first use. */
|
|
59
|
+
export interface SqliteOptions<S extends Record<string, unknown>> {
|
|
60
|
+
schema: S
|
|
61
|
+
/** `file:.data/app.db` (the default), a path, or `:memory:`. */
|
|
62
|
+
url?: string
|
|
63
|
+
/** Apply `drizzle/` before the first statement a request runs (default true). */
|
|
64
|
+
migrate?: boolean
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Drizzle over a local file, with `$weft` for the framework's own needs. */
|
|
68
|
+
export type SqliteDb<S extends Record<string, unknown>> = SqliteRemoteDatabase<S> & { $weft: DbHandle }
|
|
69
|
+
|
|
70
|
+
/** A Drizzle client on a SQLite file, with this app's migrations and write tracking. */
|
|
71
|
+
export function createDb<S extends Record<string, unknown>>({ schema, url = 'file:.data/app.db', migrate = true }: SqliteOptions<S>): SqliteDb<S> {
|
|
72
|
+
if (/^[a-z][a-z0-9+.-]+:\/\//i.test(url) && !url.startsWith('file:'))
|
|
73
|
+
throw new StartError('Q013', `\`@weft-js/start/db/sqlite\` can't open \`${url.replace(/\/\/.*@/, '//…@')}\``, 'use `file:<path>`, or `createDb` from `@weft-js/start/db/postgres` for a Postgres URL')
|
|
74
|
+
const path = url.replace(/^file:/, '')
|
|
75
|
+
let conn: Promise<Conn> | undefined
|
|
76
|
+
const connect = () => (conn ??= open(path).catch(e => { conn = undefined; throw e }))
|
|
77
|
+
const inside = new AsyncLocalStorage<boolean>()
|
|
78
|
+
let held: Promise<unknown> = Promise.resolve()
|
|
79
|
+
const handle: DbHandle = {
|
|
80
|
+
dialect: 'sqlite',
|
|
81
|
+
exempt: new Set(),
|
|
82
|
+
async migrate(list: Migration[]) {
|
|
83
|
+
const c = await connect()
|
|
84
|
+
c.exec('create table if not exists __weft_migrations (tag text primary key, at integer not null)')
|
|
85
|
+
const done = new Set((c.query('select tag from __weft_migrations', [], 'values') as string[][]).map(r => r[0]))
|
|
86
|
+
for (const m of list) {
|
|
87
|
+
if (done.has(m.tag)) continue
|
|
88
|
+
c.exec('begin')
|
|
89
|
+
try {
|
|
90
|
+
for (const s of statements(m.sql)) c.exec(s)
|
|
91
|
+
c.query('insert into __weft_migrations values (?, ?)', [m.tag, Date.now()], 'run')
|
|
92
|
+
c.exec('commit')
|
|
93
|
+
} catch (e) {
|
|
94
|
+
c.exec('rollback')
|
|
95
|
+
throw e
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
changes: (tables, o) => import('./changes-sqlite').then(m => m.sqliteChanges({ ...o, path, open, tables })),
|
|
100
|
+
}
|
|
101
|
+
const ready = gate(handle, migrate)
|
|
102
|
+
const run = async (sql: string, params: unknown[], method: Method) => {
|
|
103
|
+
touch(handle, sql)
|
|
104
|
+
await ready()
|
|
105
|
+
const c = await connect()
|
|
106
|
+
if (!inside.getStore()) await held
|
|
107
|
+
return { rows: c.query(sql, params, method) as unknown[] }
|
|
108
|
+
}
|
|
109
|
+
const batch = async (list: { sql: string; params: unknown[]; method: Method }[]) => {
|
|
110
|
+
await ready()
|
|
111
|
+
const c = await connect()
|
|
112
|
+
await held
|
|
113
|
+
c.exec('begin')
|
|
114
|
+
try {
|
|
115
|
+
const out = list.map(q => (touch(handle, q.sql), { rows: c.query(q.sql, q.params, q.method) as unknown[] }))
|
|
116
|
+
c.exec('commit')
|
|
117
|
+
return out
|
|
118
|
+
} catch (e) {
|
|
119
|
+
c.exec('rollback')
|
|
120
|
+
throw e
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const db = drizzle(run, batch, { schema })
|
|
124
|
+
const transaction = db.transaction.bind(db)
|
|
125
|
+
db.transaction = (async (...args: Parameters<typeof transaction>) => {
|
|
126
|
+
if (inside.getStore()) return transaction(...args)
|
|
127
|
+
await ready()
|
|
128
|
+
const before = held
|
|
129
|
+
let release!: () => void
|
|
130
|
+
held = new Promise<void>(ok => { release = ok })
|
|
131
|
+
await before
|
|
132
|
+
try {
|
|
133
|
+
return await inside.run(true, () => transaction(...args))
|
|
134
|
+
} finally {
|
|
135
|
+
release()
|
|
136
|
+
}
|
|
137
|
+
}) as typeof db.transaction
|
|
138
|
+
return register(db, handle)
|
|
139
|
+
}
|
package/src/derive.d.mts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** The text with HTML entities decoded. */
|
|
2
|
+
export function decode(text: string): string
|
|
3
|
+
/** One line of plain text: whitespace collapsed, no list marker in front. */
|
|
4
|
+
export function cleanText(text: string): string
|
|
5
|
+
/** At most `max` characters, cut at a sentence or a word. */
|
|
6
|
+
export function truncate(text: string, max?: number): string
|
|
7
|
+
/** `undefined`, or why this text isn't a description a person would read. */
|
|
8
|
+
export function descriptionProblem(text: string): string | undefined
|
|
9
|
+
/** A page's title and description, as a person would pick them out of its HTML. */
|
|
10
|
+
export function pageText(html: string): { title?: string; description?: string }
|
|
11
|
+
/** The site's URL, from its config or from what the host says about this deploy. */
|
|
12
|
+
export function deployUrl(site: { url?: string | undefined } | undefined, env?: Record<string, string | undefined>): string | undefined
|
|
13
|
+
/** The card title's font size, smaller for a longer title. */
|
|
14
|
+
export function cardTitleSize(title: string): number
|
|
15
|
+
/** `paths` in pages that each fit in a response header. */
|
|
16
|
+
export function pathPages(paths: string[], max?: number): string[][]
|
package/src/derive.mjs
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// What a page's HTML says about itself (research 20 §10): the automatic layer under route `meta`. A route that sets no
|
|
2
|
+
// title gets its `h1`, one that sets no description gets its first real paragraph, cleaned and cut at a sentence. Pure
|
|
3
|
+
// functions on a string, server-side only: `weft check` runs the same `descriptionProblem` on what a route wrote.
|
|
4
|
+
|
|
5
|
+
const SKIP = new Set(['script', 'style', 'template', 'noscript', 'svg', 'math', 'nav', 'aside', 'footer', 'pre', 'figure', 'table', 'ul', 'ol', 'dl', 'details', 'button', 'select', 'textarea', 'form', 'dialog', 'iframe', 'canvas', 'object'])
|
|
6
|
+
const VOID = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'source', 'track', 'wbr'])
|
|
7
|
+
const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ', mdash: '—', ndash: '–', hellip: '…', rsquo: '’', lsquo: '‘', ldquo: '“', rdquo: '”', middot: '·', copy: '©', reg: '®', trade: '™', times: '×', rarr: '→', larr: '←' }
|
|
8
|
+
|
|
9
|
+
/** `&`, `'`, `'` → their characters. An unknown name stays as written. */
|
|
10
|
+
export const decode = text => text.replace(/&(#\d+|#x[\da-f]+|[a-z]+);/gi, (all, e) => {
|
|
11
|
+
if (e[0] !== '#') return ENTITIES[e] ?? all
|
|
12
|
+
const n = e[1] === 'x' || e[1] === 'X' ? parseInt(e.slice(2), 16) : Number(e.slice(1))
|
|
13
|
+
return n > 0 && n <= 0x10ffff ? String.fromCodePoint(n) : ''
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
/** One line of plain text: whitespace collapsed, no list marker in front, no space before punctuation. */
|
|
17
|
+
export const cleanText = text => text
|
|
18
|
+
.replace(/\s+/g, ' ')
|
|
19
|
+
.replace(/\s+([.,;:!?)])(?=\s|$)/g, '$1')
|
|
20
|
+
.trim()
|
|
21
|
+
.replace(/^(?:[-*•‣–—]\s+|\d{1,3}[.)]\s+)+/, '')
|
|
22
|
+
|
|
23
|
+
/** Not the end of a sentence, though it ends in a full stop. */
|
|
24
|
+
const ABBREVIATION = /(?:^|[\s(])(?:e\.g|i\.e|etc|vs|cf|approx|no|fig|[A-Z])\.$/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* At most `max` characters: whole sentences when the first ones fit (past a third of `max`, so one short sentence
|
|
28
|
+
* isn't all that's left), else whole words and an ellipsis. Never cuts inside a word.
|
|
29
|
+
*/
|
|
30
|
+
export function truncate(text, max = 155) {
|
|
31
|
+
if (text.length <= max) return text
|
|
32
|
+
const head = text.slice(0, max + 1)
|
|
33
|
+
let end = -1
|
|
34
|
+
for (const m of head.matchAll(/[.!?…]["'”’)]?(?=\s)/g)) {
|
|
35
|
+
const at = m.index + m[0].length
|
|
36
|
+
if (at >= max / 3 && at <= max && !ABBREVIATION.test(head.slice(0, at))) end = at
|
|
37
|
+
}
|
|
38
|
+
if (end > 0) return head.slice(0, end)
|
|
39
|
+
const cut = head.slice(0, max).replace(/\s+\S*$/, '')
|
|
40
|
+
return cut.replace(/[\s,;:(–—-]+$/, '') + '…'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** `undefined`, or why this text isn't a description a person would read: markup, code, a list item or a fragment. */
|
|
44
|
+
export function descriptionProblem(text) {
|
|
45
|
+
const s = text.trim()
|
|
46
|
+
if (/^(?:[-*•+]\s|\d{1,3}[.)]\s|#{1,6}\s|>\s)/.test(s)) return 'it starts with a markdown list marker or heading'
|
|
47
|
+
if (/<\/?[a-z][^>]*>?|\/>|=>|\w\(\)|[{}]|`|\]\(|\bx = </i.test(s)) return 'it holds code or markup'
|
|
48
|
+
if ((s.match(/[<>=;$[\]{}()|\\]/g)?.length ?? 0) / Math.max(s.length, 1) > 0.05) return 'it is mostly symbols, which reads as code'
|
|
49
|
+
if (/\s[.,;:!?)](?=\s|$)/.test(s)) return 'it has a space before punctuation, the mark of inline code that was stripped out'
|
|
50
|
+
if (s.length && !/[.!?…"'”’)]$/.test(s) && s.length > 100) return 'it stops mid-sentence'
|
|
51
|
+
if (/^[a-z]/.test(s) && !/^[a-z][\w.-]*[A-Z@/:]|^[a-z]+:/.test(s)) return 'it starts in lower case, like a fragment of a longer sentence'
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A paragraph worth quoting: prose of some length, with no code in it. */
|
|
56
|
+
const prose = text => text.length >= 40 && text.includes(' ') && !descriptionProblem(truncate(text))
|
|
57
|
+
|
|
58
|
+
function tokens(html) {
|
|
59
|
+
const out = []
|
|
60
|
+
const re = /<!--[\s\S]*?-->|<(\/?)([a-zA-Z][\w:-]*)((?:[^>"']|"[^"]*"|'[^']*')*)>/g
|
|
61
|
+
let at = 0
|
|
62
|
+
for (let m; (m = re.exec(html)); at = re.lastIndex) {
|
|
63
|
+
if (m.index > at) out.push({ text: html.slice(at, m.index) })
|
|
64
|
+
if (m[2]) out.push({ close: !!m[1], tag: m[2].toLowerCase(), attrs: m[3] })
|
|
65
|
+
}
|
|
66
|
+
if (at < html.length) out.push({ text: html.slice(at) })
|
|
67
|
+
return out
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const hidden = attrs => /\s(?:hidden|aria-hidden=(?:"true"|'true'|true))(?:[\s/]|$)/.test(attrs)
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The page's own words: `{ title, description }`, either missing when the page doesn't have one. Reads `<main>` when
|
|
74
|
+
* there is one, so a header or a footer in a layout can't speak for the page. The title is the `h1`, or with none the
|
|
75
|
+
* first `h2` or `h3`; the description is the first paragraph after the `h1` that reads as prose.
|
|
76
|
+
*/
|
|
77
|
+
export function pageText(html) {
|
|
78
|
+
const open = /<main\b/i.exec(html)
|
|
79
|
+
const end = html.toLowerCase().lastIndexOf('</main>')
|
|
80
|
+
const region = open ? html.slice(open.index, end > open.index ? end : undefined) : html
|
|
81
|
+
let title, heading, description, skip, grab, buffer = ''
|
|
82
|
+
const done = t => {
|
|
83
|
+
const text = cleanText(decode(buffer))
|
|
84
|
+
grab = undefined
|
|
85
|
+
if (t === 'p') { if (prose(text)) description ??= truncate(text) }
|
|
86
|
+
else if (t !== 'h1') heading ??= text || undefined
|
|
87
|
+
else if (text && !title) { title = text; description = undefined } // the description is what follows the title
|
|
88
|
+
}
|
|
89
|
+
for (const t of tokens(region)) {
|
|
90
|
+
if (skip) {
|
|
91
|
+
if (t.tag === skip.tag && !VOID.has(t.tag) && !/\/\s*$/.test(t.attrs ?? '')) skip.depth += t.close ? -1 : 1
|
|
92
|
+
if (!skip.depth) skip = undefined
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
if (t.text !== undefined) { if (grab) buffer += t.text; continue }
|
|
96
|
+
if (!t.close && (SKIP.has(t.tag) || hidden(t.attrs)) && !VOID.has(t.tag) && !/\/\s*$/.test(t.attrs)) {
|
|
97
|
+
skip = { tag: t.tag, depth: 1 }
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
if (grab) {
|
|
101
|
+
if (t.close && t.tag === grab) done(t.tag)
|
|
102
|
+
else if (t.tag === 'br') buffer += ' '
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
if (!t.close && /^(?:h1|h2|h3|p)$/.test(t.tag)) { grab = t.tag; buffer = '' }
|
|
106
|
+
if (title && description) break
|
|
107
|
+
}
|
|
108
|
+
return { title: title ?? heading, description }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The site's public origin, when something says what it is: `site.url`, `WEFT_SITE_URL`, or what the host sets on a
|
|
113
|
+
* deploy. No request is involved, so the answer can be trusted and written into a page a CDN keeps.
|
|
114
|
+
*/
|
|
115
|
+
export function deployUrl(site, env = {}) {
|
|
116
|
+
if (site?.url) return site.url
|
|
117
|
+
const https = host => host && `https://${host}`
|
|
118
|
+
return env.WEFT_SITE_URL
|
|
119
|
+
|| https(env.VERCEL_PROJECT_PRODUCTION_URL) || https(env.VERCEL_URL)
|
|
120
|
+
|| env.URL // Netlify
|
|
121
|
+
|| env.CF_PAGES_URL || env.RENDER_EXTERNAL_URL
|
|
122
|
+
|| https(env.RAILWAY_PUBLIC_DOMAIN)
|
|
123
|
+
|| (env.FLY_APP_NAME && `https://${env.FLY_APP_NAME}.fly.dev`)
|
|
124
|
+
|| undefined
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The built-in card's title size: bigger type for a shorter title. */
|
|
128
|
+
export const cardTitleSize = title => (title.length <= 24 ? 92 : title.length <= 44 ? 76 : title.length <= 70 ? 62 : 52)
|
|
129
|
+
|
|
130
|
+
/** `paths` in pages that each fit in a response header, with room for the link to the next page. */
|
|
131
|
+
export function pathPages(paths, max = 6000) {
|
|
132
|
+
const pages = [[]]
|
|
133
|
+
let size = 0
|
|
134
|
+
for (const p of paths) {
|
|
135
|
+
if (size + p.length + 1 > max) { pages.push([]); size = 0 }
|
|
136
|
+
pages.at(-1).push(p)
|
|
137
|
+
size += p.length + 1
|
|
138
|
+
}
|
|
139
|
+
return pages
|
|
140
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// `weft dev` for an app with sockets on Bun and Deno. Nitro's dev server assumes a Node worker for websockets: its
|
|
2
|
+
// entry takes crossws's Node adapter, which refuses to load there, and a worker on either runtime can't take the
|
|
3
|
+
// upgrade. So the app runs in Vite's own process (the `self` runner), where Vite's HTTP server hands the upgrade over
|
|
4
|
+
// and the adapter works. Node, and any app without a channel or sharedState, gets none of this.
|
|
5
|
+
|
|
6
|
+
const alternate = Boolean(process.versions.bun || process.versions.deno)
|
|
7
|
+
|
|
8
|
+
export const devRunner = sockets => (alternate && sockets ? 'self' : undefined)
|
|
9
|
+
|
|
10
|
+
const GUARD = /if \("Deno" in globalThis \|\| "Bun" in globalThis\) throw new Error\([^)]*\);?/
|
|
11
|
+
|
|
12
|
+
export const devSockets = () => ({
|
|
13
|
+
name: 'weft:dev-sockets',
|
|
14
|
+
apply: 'serve',
|
|
15
|
+
enforce: 'post',
|
|
16
|
+
configResolved(config) {
|
|
17
|
+
for (const [name, environment] of Object.entries(config.environments)) {
|
|
18
|
+
const create = environment.dev?.createEnvironment
|
|
19
|
+
if (!create || environment.consumer !== 'server') continue
|
|
20
|
+
const release = hold(name)
|
|
21
|
+
environment.dev.createEnvironment = async (...args) => release(await create(...args))
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
transform: {
|
|
25
|
+
filter: { id: /crossws/, code: 'incompatible environment' },
|
|
26
|
+
handler: code => code.replace(GUARD, ''),
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// Nitro can name an environment to the runner before Vite has made it. Across a process that takes long enough, but
|
|
31
|
+
// `self` answers at once: the first request goes to nothing and the environment hangs for good. So each one is held
|
|
32
|
+
// until it exists, and `init` names it again. (The worker keeps these on globalThis, and `self` is this process.)
|
|
33
|
+
const HELD = { hasEvaluated: () => false, reload: async () => {}, fetch: () => Promise.resolve(new Response('starting', { status: 503 })) }
|
|
34
|
+
|
|
35
|
+
function hold(name) {
|
|
36
|
+
const environments = (globalThis.__nitro_vite_envs__ ??= {})
|
|
37
|
+
environments[name] = HELD
|
|
38
|
+
return environment => {
|
|
39
|
+
environments[name] = undefined
|
|
40
|
+
return environment
|
|
41
|
+
}
|
|
42
|
+
}
|