@weft-js/start 0.1.1 → 0.1.2

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.
Files changed (213) hide show
  1. package/README.md +36 -3
  2. package/crate/Cargo.lock +1347 -0
  3. package/crate/Cargo.toml +72 -0
  4. package/crate/rust-toolchain.toml +3 -0
  5. package/crate/src/build/emit.rs +103 -0
  6. package/crate/src/build.rs +367 -0
  7. package/crate/src/ctx.rs +186 -0
  8. package/crate/src/envelope.rs +163 -0
  9. package/crate/src/functions.rs +173 -0
  10. package/crate/src/lib.rs +118 -0
  11. package/crate/src/native.rs +212 -0
  12. package/crate/src/otel.rs +62 -0
  13. package/crate/src/server.rs +203 -0
  14. package/package.json +46 -37
  15. package/src/app.mjs +38 -8
  16. package/src/asset-receipt.d.mts +13 -0
  17. package/src/asset-receipt.mjs +61 -0
  18. package/src/auth/better-auth.ts +215 -17
  19. package/src/auth/drizzle.ts +1 -1
  20. package/src/auth.ts +21 -6
  21. package/src/boot.ts +19 -8
  22. package/src/bus/postgres.ts +32 -19
  23. package/src/bus.ts +7 -4
  24. package/src/{capture.mjs → capture.ts} +34 -1
  25. package/src/changes-sql.d.mts +8 -2
  26. package/src/changes-sql.mjs +26 -10
  27. package/src/changes.ts +165 -66
  28. package/src/channel/connections.ts +216 -0
  29. package/src/channel/socket.ts +396 -0
  30. package/src/channel-fake.ts +19 -4
  31. package/src/channel.ts +120 -492
  32. package/src/client.tsx +9 -2
  33. package/src/contract/describe.mjs +231 -0
  34. package/src/contract.mjs +144 -157
  35. package/src/data/cache.ts +354 -0
  36. package/src/data/definitions.ts +94 -0
  37. package/src/data/draft.ts +104 -0
  38. package/src/data/foundation.ts +95 -0
  39. package/src/data/live-api.ts +22 -0
  40. package/src/data/mutation.ts +138 -0
  41. package/src/data/primitives.ts +135 -0
  42. package/src/data/query.ts +106 -0
  43. package/src/data/transport.ts +121 -0
  44. package/src/data/types.ts +303 -0
  45. package/src/data.ts +69 -894
  46. package/src/db/bun-sqlite.d.ts +4 -1
  47. package/src/db/changes-mysql.ts +61 -21
  48. package/src/db/changes-postgres.ts +197 -118
  49. package/src/db/changes-sqlite.ts +131 -64
  50. package/src/db/core.ts +38 -13
  51. package/src/db/index.ts +20 -4
  52. package/src/db/mysql.ts +22 -6
  53. package/src/db/postgres.ts +49 -13
  54. package/src/db/sqlite.ts +53 -27
  55. package/src/derive.d.mts +4 -1
  56. package/src/derive.mjs +136 -39
  57. package/src/dev-runtime.mjs +8 -4
  58. package/src/exec/errors.ts +57 -0
  59. package/src/exec/landing.ts +66 -0
  60. package/src/exec/scope.ts +174 -0
  61. package/src/exec/touch.ts +80 -0
  62. package/src/exec/validation.ts +176 -0
  63. package/src/exec.ts +191 -341
  64. package/src/extension.ts +8 -2
  65. package/src/first-paint.ts +26 -9
  66. package/src/form/value.ts +15 -0
  67. package/src/form.tsx +132 -43
  68. package/src/hub/channels.ts +102 -0
  69. package/src/hub/connections.ts +147 -0
  70. package/src/hub/delivery.ts +62 -0
  71. package/src/hub/feed.ts +168 -0
  72. package/src/hub/presence.ts +64 -0
  73. package/src/hub/route.ts +158 -0
  74. package/src/hub/subscriptions.ts +212 -0
  75. package/src/hub/topic.ts +98 -0
  76. package/src/hub/tracked.ts +12 -0
  77. package/src/hub/types.ts +190 -0
  78. package/src/hub/view.ts +147 -0
  79. package/src/hub.ts +132 -758
  80. package/src/index.ts +84 -10
  81. package/src/inline.mjs +356 -0
  82. package/src/inspect/compare.mjs +162 -0
  83. package/src/inspect/routes.mjs +154 -0
  84. package/src/inspect/samples.mjs +59 -0
  85. package/src/inspect/url.mjs +2 -0
  86. package/src/inspect.mjs +146 -187
  87. package/src/link/election.ts +178 -0
  88. package/src/link/lines.ts +27 -0
  89. package/src/link.ts +90 -166
  90. package/src/list-drizzle.ts +104 -18
  91. package/src/list.ts +127 -36
  92. package/src/listen.ts +32 -6
  93. package/src/live/api.ts +18 -0
  94. package/src/live/backoff.ts +6 -0
  95. package/src/live/frames.ts +42 -0
  96. package/src/live/read.ts +77 -0
  97. package/src/live/sub-types.ts +24 -0
  98. package/src/live/transport.ts +86 -0
  99. package/src/live/types.ts +52 -0
  100. package/src/live-fake.ts +20 -5
  101. package/src/live-target.mjs +112 -37
  102. package/src/live.ts +116 -225
  103. package/src/media.ts +8 -1
  104. package/src/migrations.mjs +14 -6
  105. package/src/mux.ts +161 -64
  106. package/src/og/DefaultCard.tsx +40 -9
  107. package/src/og/fonts.mjs +16 -8
  108. package/src/og/images.mjs +43 -22
  109. package/src/og/index.mjs +119 -43
  110. package/src/og/native.mjs +11 -4
  111. package/src/range.mjs +15 -3
  112. package/src/router.ts +56 -6
  113. package/src/routes.mjs +73 -22
  114. package/src/run/admission.ts +80 -0
  115. package/src/run/boot.tsx +115 -0
  116. package/src/run/cache-presentation.ts +67 -0
  117. package/src/run/cache.ts +254 -0
  118. package/src/run/helpers.ts +197 -0
  119. package/src/run/listeners.ts +155 -0
  120. package/src/run/navigation.ts +382 -0
  121. package/src/run/page-skeleton.tsx +35 -0
  122. package/src/run/prefetch.ts +128 -0
  123. package/src/run/retain-view.ts +18 -0
  124. package/src/run/shape-preparation.ts +97 -0
  125. package/src/run/shape-styles.ts +72 -0
  126. package/src/run/source-context/pin.ts +124 -0
  127. package/src/run/source-context.ts +311 -0
  128. package/src/run.tsx +50 -178
  129. package/src/rust/common.ts +163 -0
  130. package/src/rust/native.ts +282 -0
  131. package/src/rust.ts +103 -229
  132. package/src/server/card.tsx +65 -0
  133. package/src/server/compression.ts +115 -0
  134. package/src/server/core.ts +70 -0
  135. package/src/server/dispatch.ts +70 -0
  136. package/src/server/form.ts +85 -0
  137. package/src/server/functions.ts +129 -0
  138. package/src/server/handler.ts +39 -0
  139. package/src/server/head.ts +96 -0
  140. package/src/server/media.ts +60 -0
  141. package/src/server/navigation-cache.ts +80 -0
  142. package/src/server/navigation.tsx +85 -0
  143. package/src/server/page-cold.ts +95 -0
  144. package/src/server/page-stream.tsx +181 -0
  145. package/src/server/page-types.ts +51 -0
  146. package/src/server/page-warm.ts +130 -0
  147. package/src/server/page.ts +69 -0
  148. package/src/server/payload.ts +89 -0
  149. package/src/server/prerender.ts +60 -0
  150. package/src/server/preview.tsx +67 -0
  151. package/src/server/render.ts +167 -0
  152. package/src/server/request.ts +98 -0
  153. package/src/server/resolver.ts +228 -0
  154. package/src/server/routes.ts +150 -0
  155. package/src/server/services.ts +69 -0
  156. package/src/server/text.ts +29 -0
  157. package/src/server/types.ts +160 -0
  158. package/src/server.tsx +82 -971
  159. package/src/shape.mjs +30 -6
  160. package/src/share/display.ts +50 -0
  161. package/src/share/network.ts +119 -0
  162. package/src/share/receive.ts +70 -0
  163. package/src/share/state.ts +30 -0
  164. package/src/share/tab-queue.ts +28 -0
  165. package/src/share-server.ts +128 -43
  166. package/src/share.ts +103 -209
  167. package/src/sheets.mjs +30 -18
  168. package/src/sidecar/build.mjs +170 -0
  169. package/src/sidecar/cargo.mjs +164 -0
  170. package/src/sidecar/common.mjs +17 -0
  171. package/src/sidecar/dev-build.mjs +91 -0
  172. package/src/sidecar/dev-watch.mjs +88 -0
  173. package/src/sidecar/manifest.mjs +19 -0
  174. package/src/sidecar/process.mjs +58 -0
  175. package/src/sidecar/refresh.mjs +19 -0
  176. package/src/sidecar.mjs +75 -416
  177. package/src/socket/check.ts +50 -0
  178. package/src/socket/deliver.ts +16 -0
  179. package/src/socket/publish.ts +56 -0
  180. package/src/socket/rooms.ts +41 -0
  181. package/src/socket/route.ts +79 -0
  182. package/src/socket/shared.ts +26 -0
  183. package/src/socket/types.ts +48 -0
  184. package/src/socket.ts +205 -194
  185. package/src/stream.ts +10 -1
  186. package/src/verify/app.ts +39 -0
  187. package/src/verify/cleanup.ts +27 -0
  188. package/src/verify/database.ts +11 -0
  189. package/src/verify/live.ts +63 -0
  190. package/src/verify/notify.ts +35 -0
  191. package/src/verify/skip.ts +19 -0
  192. package/src/verify/support.ts +149 -0
  193. package/src/verify/timing.ts +15 -0
  194. package/src/verify/trigger.ts +14 -0
  195. package/src/verify.ts +77 -176
  196. package/src/view.tsx +96 -11
  197. package/src/vite/asset-bytes.mjs +95 -0
  198. package/src/vite/assets.mjs +117 -0
  199. package/src/vite/config-hooks.mjs +166 -0
  200. package/src/vite/dev-hooks.mjs +103 -0
  201. package/src/vite/dev.mjs +102 -0
  202. package/src/vite/entries.mjs +145 -0
  203. package/src/vite/module-hooks.mjs +288 -0
  204. package/src/vite/route-modules.mjs +289 -0
  205. package/src/vite/server-functions.mjs +222 -0
  206. package/src/vite/shape-entry-css.mjs +187 -0
  207. package/src/vite/shape-override.mjs +226 -0
  208. package/src/vite/shared.mjs +51 -0
  209. package/src/vite/sheets.mjs +92 -0
  210. package/src/vite/state.mjs +89 -0
  211. package/src/vite/syntax.mjs +139 -0
  212. package/src/vite.mjs +152 -853
  213. package/src/capture.d.mts +0 -1
@@ -0,0 +1,102 @@
1
+ import { existsSync, readFileSync, readdirSync, realpathSync } from 'node:fs'
2
+ import { createRequire } from 'node:module'
3
+ import { dirname, join } from 'node:path'
4
+ import { here } from './shared.mjs'
5
+
6
+ /** Dev, in place of the bare JSON a page gets when its code won't load: this tab can't take an update, so it reloads. */
7
+ const NOT_LOADING = `<!doctype html><meta charset="utf-8"><title>This page failed to load</title>
8
+ <body style="font:16px system-ui;padding:2rem"><h1>This page failed to load</h1><p>The error is in the terminal. This tab reloads when you save a fix.</p>
9
+ <script type="module">
10
+ import { createHotContext } from '/@vite/client'
11
+ const hot = createHotContext('/@weft/not-loading')
12
+ for (const event of ['vite:afterUpdate', 'weft:refresh']) hot.on(event, () => location.reload())
13
+ </script>`
14
+
15
+ export function waitingForAFix(req, res, next) {
16
+ if (!req.headers.accept?.includes('text/html')) return next()
17
+ const { writeHead, end } = res
18
+ const json = (headers) =>
19
+ /json/.test(
20
+ String(
21
+ Array.isArray(headers)
22
+ ? headers.flat().join(' ')
23
+ : headers && Object.entries(headers).flat().join(' '),
24
+ ) + res.getHeader('content-type'),
25
+ )
26
+ let failed = false
27
+ res.writeHead = function (status, ...rest) {
28
+ failed = status >= 500 && json(rest.at(-1))
29
+ if (!failed) return writeHead.call(this, status, ...rest)
30
+ res.statusCode = status
31
+ return this
32
+ }
33
+ const write = res.write
34
+ res.write = function (...args) {
35
+ return failed ? true : write.apply(this, args)
36
+ }
37
+ res.end = function (...args) {
38
+ if (!failed && !(res.statusCode >= 500 && !res.headersSent && json()))
39
+ return end.apply(this, args)
40
+ res.setHeader('content-type', 'text/html')
41
+ res.removeHeader('content-length')
42
+ return end.call(this, NOT_LOADING)
43
+ }
44
+ next()
45
+ }
46
+
47
+ /** Whether an update to `mod` stops at a module that accepts it. */
48
+ export function reaches(mod, seen = new Set()) {
49
+ if (mod.isSelfAccepting || mod.acceptedHmrDeps?.size) return true
50
+ if (seen.has(mod)) return true
51
+ seen.add(mod)
52
+ return (
53
+ mod.importers.size > 0 &&
54
+ [...mod.importers].every((m) => m.acceptedHmrDeps?.has(mod) || reaches(m, seen))
55
+ )
56
+ }
57
+
58
+ /** Ask open pages for the server's new answer, once the server's own reload has been told. A burst of saves is one ask. */
59
+ const asking = new WeakMap()
60
+ export function refresh(vite, rel, say = true, queries = true) {
61
+ if (!vite) return
62
+ clearTimeout(asking.get(vite))
63
+ asking.set(
64
+ vite,
65
+ setTimeout(() => {
66
+ if (say)
67
+ vite.config.logger.info(
68
+ `weft: ${rel} runs on the server: refreshing the page's data, state kept`,
69
+ { timestamp: true },
70
+ )
71
+ vite.ws.send({ type: 'custom', event: 'weft:refresh', data: { queries } })
72
+ }, 50),
73
+ )
74
+ }
75
+
76
+ export function packageDirs(root) {
77
+ const req = createRequire(join(root, 'package.json'))
78
+ const own = createRequire(join(here, 'vite.mjs'))
79
+ const out = []
80
+ for (const name of ['@weft-js/core/vite', 'nitro/package.json']) {
81
+ // each at its package root
82
+ for (const r of [req, own]) {
83
+ try {
84
+ out.push(dirname(realpathSync(r.resolve(name))))
85
+ break
86
+ } catch {}
87
+ }
88
+ }
89
+ return out
90
+ }
91
+
92
+ /** The project asks for shapes: a `pending="shape"`, or a read of `.shape`. */
93
+ export function usesShapes(root) {
94
+ const src = join(root, 'src')
95
+ if (!existsSync(src)) return false
96
+ return readdirSync(src, { recursive: true, withFileTypes: true }).some(
97
+ (f) =>
98
+ f.isFile() &&
99
+ /\.[jt]sx?$/.test(f.name) &&
100
+ /pending="shape"|\.shape\b/.test(readFileSync(join(f.parentPath, f.name), 'utf8')),
101
+ )
102
+ }
@@ -0,0 +1,145 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { createRequire } from 'node:module'
3
+ import { join } from 'node:path'
4
+ import { scanRequired } from '../live-target.mjs'
5
+ import { here, V, purgeFile, INLINE_SHEETS, SHEET_PIECES, ROUTE_ASSET_BYTES } from './shared.mjs'
6
+
7
+ export function serverEntry(clientEntry, dev, root) {
8
+ return `import { createHandler } from ${JSON.stringify(join(here, 'server.tsx'))}
9
+ import { chunks, lazy, manifest, modules, routeCss, statics } from '${V.routes}'
10
+ import '${V.fns}'
11
+ import context from '${V.context}'
12
+ import auth from '${V.auth}'
13
+ import migrations from '${V.migrations}'
14
+ import media from '${V.media}'
15
+ import realtime from '${V.realtime}'
16
+ import extensions from '${V.extensions}'
17
+ import * as app from '${V.app}'
18
+ import assets from '${clientEntry}?assets=client'
19
+ import { preloads } from 'weft:fonts/preloads'
20
+ ${dev ? '' : `import { parse, purge } from ${JSON.stringify(purgeFile)}\n`}const fetch = createHandler({ manifest, modules, statics, chunks, lazy, context, auth, migrations, media, realtime, liveRequired: ${JSON.stringify(scanRequired(root).map((r) => r.name))}, extensions, site: app.site, build: app.build, dev: app.dev, fonts: app.fonts(), timeout: app.timeout, hold: app.hold, routeAssetBytes: ${dev ? 'null' : `'${ROUTE_ASSET_BYTES}'`}, assets: { entry: assets.entry, js: assets.js, get css() { return routeCss(assets.css) }, fonts: preloads, inline: ${dev ? 'undefined' : `'${INLINE_SHEETS}'`}, pieces: ${dev ? 'undefined' : `'${SHEET_PIECES}'`}, critical: ${dev ? 'undefined' : '{ parse, purge }'} },
21
+ // H005–H008, Q002–Q014, A002–A004, F002–F003, M017 and R-codes found while serving: the server log, in dev and production.
22
+ onDiagnostic: d => console.warn(\`[weft] \${d.code} \${d.severity}\${d.route ? \` (\${d.route})\` : ''}: \${d.message.replace(/^[A-Z]{1,3}\\d{3}:? /, '')}\`) })
23
+ export default { fetch }
24
+ ${dev ? "import { acceptScopeReruns } from '@weft-js/core/hmr'\nacceptScopeReruns()\n" : ''}`
25
+ }
26
+
27
+ /**
28
+ * A `server.extension` provider runs only for an app with `src/<key>.server.ts`, the file that says it wants it (P118):
29
+ * the provider is loaded on the first request and handed that file's default export. No provider gives an empty list.
30
+ */
31
+ export function extensionsModule(root, abs) {
32
+ const slot = join(root, '.weft', 'slots', 'server.extension.ts')
33
+ const keys = existsSync(slot)
34
+ ? Object.entries(
35
+ JSON.parse(/providers = (\{.*\}) as const/.exec(readFileSync(slot, 'utf8'))?.[1] ?? '{}'),
36
+ )
37
+ .filter(([, name]) => name)
38
+ .map(([key]) => key)
39
+ : []
40
+ const files = keys.flatMap((key) => {
41
+ const f = ['ts', 'mts', 'js', 'mjs']
42
+ .map((x) => `src/${key}.server.${x}`)
43
+ .find((x) => existsSync(abs(x)))
44
+ return f ? [[key, abs(f)]] : []
45
+ })
46
+ if (!files.length) return 'export default []\n'
47
+ return `import { load } from 'weft:slot/server.extension'\n${files.map(([, f], i) => `import app${i} from ${JSON.stringify(f)}\n`).join('')}export default [${files.map(([key], i) => `{ key: ${JSON.stringify(key)}, load: () => load(${JSON.stringify(key)}).then(extend => extend(app${i})) }`).join(', ')}]\n`
48
+ }
49
+
50
+ /** Dev serves from the project; a built server from the directory it runs in (or `WEFT_MEDIA_DIR`). */
51
+ export function mediaModule(root, dev) {
52
+ const slot = join(root, '.weft', 'slots', 'media.server.ts')
53
+ if (!existsSync(slot) || readFileSync(slot, 'utf8').includes('provider = null'))
54
+ return 'export default undefined\n'
55
+ const at = dev ? JSON.stringify(root) : 'undefined'
56
+ if (!existsSync(join(root, '.weft', 'slots', 'media.encode.ts')))
57
+ return `import { load } from 'weft:slot/media.server'\nexport default () => load().then(m => m.at(${at}))\n`
58
+ return `import { load } from 'weft:slot/media.server'
59
+ import { load as encoder, providers } from 'weft:slot/media.encode'
60
+ const encode = async () => Object.fromEntries(await Promise.all(Object.keys(providers).filter(k => providers[k]).map(async k => [k, await encoder(k)])))
61
+ export default () => Promise.all([load(), encode()]).then(([m, e]) => m.at(${at}, e))
62
+ `
63
+ }
64
+
65
+ export const busBound = (root) => {
66
+ const slot = join(root, '.weft', 'slots', 'realtime.bus.ts')
67
+ return existsSync(slot) && readFileSync(slot, 'utf8').includes('provider = {')
68
+ }
69
+
70
+ /** A bus in `src/realtime.server.ts` wins over the plugin's, like any explicit choice. */
71
+ export const busModule = (file, from) => `import { load, provider } from 'weft:slot/realtime.bus'
72
+ import { slotBus } from ${JSON.stringify(join(from, 'bus.ts'))}
73
+ ${file ? `import own from ${JSON.stringify(file)}` : 'const own = undefined'}
74
+ export default { ...own, bus: own?.bus ?? slotBus(provider.plugin, load) }
75
+ `
76
+
77
+ export function appModule({ site, build, dev, fonts, timeout, hold }) {
78
+ return `export const site = ${JSON.stringify(site)}
79
+ export const build = ${JSON.stringify(build)}
80
+ export const dev = ${dev}
81
+ export const timeout = ${timeout ?? 'undefined'}
82
+ export const hold = ${hold ?? 'undefined'}
83
+ const raw = ${JSON.stringify(fonts)}
84
+ let decoded
85
+ export function fonts() {
86
+ return (decoded ??= raw.map(f => ({ ...f, data: Uint8Array.from(atob(f.data), c => c.charCodeAt(0)) })))
87
+ }
88
+ `
89
+ }
90
+
91
+ /**
92
+ * The fonts every card can use, resolved at build and shipped inside the server bundle (research 20 §4.2: a render
93
+ * never fetches fonts). `Inter:400,700` → `@fontsource/inter`'s latin WOFF2 files, from the app or from @weft-js/start.
94
+ */
95
+ export function cardFonts(manifest, abs, defaults, root, own = {}) {
96
+ const specs = new Set(defaults)
97
+ for (const f of [...manifest.routes.map((r) => r.card), manifest.defaultCard].filter(Boolean)) {
98
+ const m = /export\s+const\s+fonts\s*=\s*\[([^\]]*)\]/.exec(readFileSync(abs(f), 'utf8'))
99
+ if (m) for (const s of m[1].matchAll(/['"]([^'"]+)['"]/g)) specs.add(s[1])
100
+ }
101
+ const requires = [
102
+ createRequire(join(root, 'package.json')),
103
+ createRequire(join(here, 'vite.mjs')),
104
+ ]
105
+ const out = []
106
+ // The app's own faces (`site.card.fonts`, filled in by the text plugin): the card's title and text look like the app.
107
+ for (const f of [own.display, own.body].filter(Boolean)) {
108
+ const path = join(root, f.src)
109
+ if (existsSync(path) && !out.some((x) => x.name === f.family))
110
+ out.push({
111
+ name: f.family,
112
+ weight: f.weight ?? 400,
113
+ style: 'normal',
114
+ data: readFileSync(path).toString('base64'),
115
+ })
116
+ }
117
+ for (const spec of specs) {
118
+ const [family, weights = '400'] = spec.split(':')
119
+ const pkg = family.toLowerCase().replace(/\s+/g, '-')
120
+ for (const w of weights.split(',')) {
121
+ const [weight, italic] = [Number.parseInt(w), /i(talic)?$/.test(w)]
122
+ const file = `@fontsource/${pkg}/files/${pkg}-latin-${weight}-${italic ? 'italic' : 'normal'}.woff2`
123
+ const path = requires
124
+ .map((r) => {
125
+ try {
126
+ return r.resolve(file)
127
+ } catch {
128
+ return undefined
129
+ }
130
+ })
131
+ .find(Boolean)
132
+ if (!path)
133
+ throw new Error(
134
+ `weft: card font \`${spec}\` needs \`@fontsource/${pkg}\` installed (looked for ${file})`,
135
+ )
136
+ out.push({
137
+ name: family,
138
+ weight,
139
+ style: italic ? 'italic' : 'normal',
140
+ data: readFileSync(path).toString('base64'),
141
+ })
142
+ }
143
+ }
144
+ return out
145
+ }
@@ -0,0 +1,288 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { createRequire } from 'node:module'
3
+ import { dirname, join, relative, resolve } from 'node:path'
4
+ import { readMigrations } from '../migrations.mjs'
5
+ import { routeTypes } from '../routes.mjs'
6
+ import { assetsOnly } from './assets.mjs'
7
+ import { entryCss, projectParent, watchEntryCandidates } from './shape-entry-css.mjs'
8
+ import { resolveImportedOverride, shapeOverride } from './shape-override.mjs'
9
+ import {
10
+ clientRoutes,
11
+ serverRoutes,
12
+ devStyles,
13
+ markLinks,
14
+ skeletonExport,
15
+ } from './route-modules.mjs'
16
+ import {
17
+ serverEntry,
18
+ extensionsModule,
19
+ mediaModule,
20
+ busBound,
21
+ busModule,
22
+ appModule,
23
+ cardFonts,
24
+ } from './entries.mjs'
25
+ import { stripServerExports, serverExports } from './syntax.mjs'
26
+ import { isServerModule, definesFns, withIds, clientStubs } from './server-functions.mjs'
27
+ import { here, V, DEFAULT_FONTS, ASSETS, ISLAND, ENTRY_SHEETS } from './shared.mjs'
28
+
29
+ export const resolveId = (s) => ({
30
+ filter: {
31
+ id: /^(virtual:weft\/|@weft-js\/start\/(db|auth|bus|list|rust|sidecar)(\/|$)|@weft-js\/(realtime-(redis|nats|durable-object)|db-[\w-]+)$|sharp$)/,
32
+ },
33
+ handler(source, importer) {
34
+ // Sharp's addon can't be bundled, so it stays external and Nitro traces it, which it can only do from the app.
35
+ if (source === 'sharp') {
36
+ if (s.dev || !importer?.includes('plugin-media')) return
37
+ try {
38
+ createRequire(join(s.root, 'package.json')).resolve('sharp')
39
+ } catch {
40
+ this.warn(
41
+ "M017: `sharp` isn't a dependency of this app, so the built server can't load it for the media route.\n fix: add `sharp` to dependencies",
42
+ )
43
+ return
44
+ }
45
+ return { id: 'sharp', external: true }
46
+ }
47
+ if (source.startsWith('@weft-js/')) {
48
+ if (this.environment?.config.consumer === 'client')
49
+ this.error(
50
+ `Q012: \`${source}\` reached the client graph${importer ? ` from ${relative(s.root, importer)}` : ''}: the database, auth and buses run on the server only.\n fix: import it from a server module (src/server/**, *.server.ts) or a route's \`load\``,
51
+ )
52
+ return
53
+ }
54
+ const base = source.replace(/\?.*$/, '')
55
+ if (
56
+ Object.values(V).includes(base) ||
57
+ base.startsWith(ISLAND) ||
58
+ base.startsWith('virtual:weft/shape-override/')
59
+ )
60
+ return s.id(source)
61
+ },
62
+ })
63
+
64
+ export const load = (s) => ({
65
+ filter: { id: /^\0virtual:weft\// },
66
+ async handler(raw) {
67
+ // `virtual:weft/entry-client?assets=client` (no src/entry.client.ts) is Nitro's assets plugin's to load.
68
+ if (/[?&]assets\b/.test(raw)) return
69
+ const which = raw.slice(1).replace(/\?.*$/, '')
70
+ const client = this.environment?.config.consumer === 'client'
71
+ if (which.startsWith('virtual:weft/shape-override/'))
72
+ return overrideModule(this, s, which, client)
73
+ if (which.startsWith(ISLAND)) {
74
+ const { islands } = await s.hydration(this)
75
+ const i = islands[Number(which.slice(ISLAND.length))]
76
+ return `export { $i${i.index} as default } from ${JSON.stringify(i.file)}\n`
77
+ }
78
+ if (which === V.routes) return routesModule(this, s, client)
79
+ if (which === V.app)
80
+ return appModule({
81
+ site: s.options.site ?? {},
82
+ build: s.build,
83
+ dev: s.dev,
84
+ fonts: cardFonts(
85
+ s.manifest,
86
+ s.abs,
87
+ s.options.fonts ?? DEFAULT_FONTS,
88
+ s.root,
89
+ s.options.site?.card?.fonts,
90
+ ),
91
+ timeout: s.options.timeout,
92
+ hold: s.options.hold,
93
+ })
94
+ if (which === V.server) return serverEntry(s.clientEntry(), s.dev, s.root)
95
+ if (which === V.fns)
96
+ return s.serverModules.map((f) => `import ${JSON.stringify(s.abs(f))}`).join('\n') + '\n'
97
+ if (which === V.context || which === V.auth || which === V.realtime)
98
+ return configModule(s, which)
99
+ if (which === V.extensions) return extensionsModule(s.root, s.abs)
100
+ if (which === V.migrations)
101
+ return s.dev
102
+ ? `import { readMigrations } from ${JSON.stringify(join(here, 'migrations.mjs'))}\nexport default () => readMigrations(${JSON.stringify(s.root)})\n`
103
+ : `const list = ${JSON.stringify(readMigrations(s.root))}\nexport default () => list\n`
104
+ if (which === V.media) return mediaModule(s.root, s.dev)
105
+ if (which === V.client)
106
+ return `import { start } from ${JSON.stringify(join(here, 'client.tsx'))}\nimport * as routes from '${V.routes}'\n${ENTRY_SHEETS}start(routes)\n`
107
+ },
108
+ })
109
+
110
+ export const transform = (s) => ({
111
+ filter: { id: /\.[jt]sx?(\?weft-assets)?$|\/vite\/dist\/client\/client\.mjs$/ },
112
+ async handler(code, file) {
113
+ const path = file.replace(/\?.*$/, '')
114
+ if (file.endsWith(ASSETS))
115
+ return { code: await assetsOnly(this, code, path, s.root, s.words), map: null }
116
+ if (path.endsWith('/client.mjs')) return s.dev ? markLinks(s.root) + code : undefined
117
+ if (path.includes('/node_modules/')) return
118
+ const client = this.environment?.config.consumer === 'client'
119
+ if (client && path === s.clientEntry()) return { code: ENTRY_SHEETS + code, map: null }
120
+ const rel = relative(s.root, path).split('\\').join('/')
121
+ if (isServerModule(rel)) {
122
+ const out = client
123
+ ? clientStubs(code, path, rel, await s.shapeTable(this))
124
+ : withIds(code, path, rel)
125
+ if (client && s.dev) s.stubs.set(path, out)
126
+ return out === code ? undefined : { code: out, map: null }
127
+ }
128
+ if (client && !rel.startsWith('..')) {
129
+ const q003 = definesFns(code, path)
130
+ if (q003)
131
+ this.error(
132
+ `Q003: ${rel} defines \`${q003.name}\` with \`${q003.kind}()\`, but it isn't a server module: its \`run\` and imports would ship to the client.\n fix: move it to src/server/ or a *.server.ts file`,
133
+ )
134
+ }
135
+ if (!client) return
136
+ if (
137
+ !s.manifest.routes.some((r) => r.file === rel) &&
138
+ !s.manifest.layouts.some((l) => l.file === rel)
139
+ )
140
+ return
141
+ if (s.dev) s.loaders.set(file, serverExports(code, file))
142
+ const out = stripServerExports(code, file)
143
+ // @weft-js/core/hmr saw the server exports and left this to Vite, but they're gone and the rest is components.
144
+ if (
145
+ s.dev &&
146
+ out !== code &&
147
+ !/^export (?!default\b|\{ \S+\$shape as default\$shape \})/m.test(out) &&
148
+ !out.includes('import.meta.hot.accept(')
149
+ )
150
+ return { code: `${out}\nif (import.meta.hot) import.meta.hot.accept();\n`, map: null }
151
+ return out === code ? undefined : { code: out, map: null }
152
+ },
153
+ })
154
+
155
+ export const api = (s) => ({
156
+ manifest: () => s.manifest ?? s.scan(),
157
+ types: () => routeTypes(s.manifest ?? s.scan()),
158
+ serverModules: () => (s.manifest ?? s.scan(), s.serverModules),
159
+ })
160
+
161
+ /** What a route's imported skeleton gave: a file to watch, an override to use, or nothing (said so). */
162
+ function took(ctx, { s, root, file, found, depend, overrides }) {
163
+ if (found?.file) depend(found.file, file)
164
+ if (found?.code) overrides.set(file, found.code)
165
+ // An imported function that isn't plain markup is as unusable as a local one.
166
+ else unusableSkeleton(ctx, s, root, file)
167
+ }
168
+
169
+ /** A route that exports `skeleton` in a form navigation can't use gets none at all: say so once, with what works. */
170
+ function unusableSkeleton(ctx, s, root, file) {
171
+ if (!/export\s+const\s+skeleton\b/.test(readFileSync(file, 'utf8'))) return
172
+ const warned = (s.skeletonWarned ??= new Set())
173
+ if (warned.has(file)) return
174
+ warned.add(file)
175
+ ctx.warn(
176
+ `${relative(root, file)}: \`export const skeleton\` can't be used, so this route shows no skeleton. ` +
177
+ 'Name a function in this file (or one imported from its own file) whose JSX is plain markup: div, span, section, ' +
178
+ 'article, main, header, footer, p, h1-h6, ul, ol, li, with only string class and data-* attributes. ' +
179
+ '`export const skeleton = false` turns it off. `weft docs start` has an example.',
180
+ )
181
+ }
182
+
183
+ async function overrideModule(ctx, s, which, client) {
184
+ if (!client || !which.endsWith('.tsx')) ctx.error('Invalid skeleton override request')
185
+ const file = decodeURIComponent(which.slice('virtual:weft/shape-override/'.length, -4))
186
+ const levels = [...s.manifest.layouts, ...s.manifest.routes]
187
+ if (!levels.some((level) => s.abs(level.file) === file)) ctx.error('Unknown skeleton override')
188
+ const imported = await resolveImportedOverride(ctx, file, s.root ?? dirname(file))
189
+ const code = shapeOverride(file) ?? imported?.code
190
+ if (!code) ctx.error(`Unsafe or missing skeleton override: ${file}`)
191
+ ctx.addWatchFile(file)
192
+ if (imported?.file) ctx.addWatchFile(imported.file)
193
+ return code
194
+ }
195
+
196
+ function configModule(s, which) {
197
+ const name = which.slice('virtual:weft/'.length)
198
+ const f = ['ts', 'mts', 'js', 'mjs']
199
+ .map((x) => `src/${name}.server.${x}`)
200
+ .find((x) => existsSync(s.abs(x)))
201
+ if (which === V.realtime && busBound(s.root)) return busModule(f && s.abs(f), here)
202
+ return f ? `export { default } from ${JSON.stringify(s.abs(f))}\n` : 'export default undefined\n'
203
+ }
204
+
205
+ async function routesModule(ctx, s, client) {
206
+ const run = { owner: s.shapeEntryOwner, scan: undefined }
207
+ if (client && s.shapeEntryClosed) throw new Error('entry style scan outlived its server')
208
+ if (client)
209
+ run.scan = s.shapeScanActive = s.shapeScanGeneration = (s.shapeScanGeneration ?? 0) + 1
210
+ try {
211
+ const p = await s.hydration(ctx)
212
+ if (client) return await clientRoutesModule(ctx, s, p, run)
213
+ return serverRoutes(
214
+ s.manifest,
215
+ s.abs,
216
+ s.dev
217
+ ? await devStyles(s.vite.environments.client, s.manifest, s.abs, s.clientEntry())
218
+ : undefined,
219
+ p,
220
+ )
221
+ } finally {
222
+ if (client && s.shapeScanActive === run.scan) delete s.shapeScanActive
223
+ }
224
+ }
225
+
226
+ async function scanOverrides(ctx, s, all, root) {
227
+ const overrides = new Map()
228
+ const dependencies = new Map()
229
+ const missing = new Set()
230
+ const depend = (target, file) => {
231
+ if (!dependencies.has(target)) dependencies.set(target, new Set())
232
+ dependencies.get(target).add(file)
233
+ }
234
+ for (const file of all) {
235
+ if (!/\.[jt]sx$/.test(file) || skeletonExport(file) !== 'unavailable') continue
236
+ const found = await resolveImportedOverride(ctx, file, root, (spec) => {
237
+ const candidate = resolve(dirname(file), spec)
238
+ if (projectParent(root, candidate)) {
239
+ const path = candidate.replace(/\\/g, '/')
240
+ missing.add(path)
241
+ depend(path, file)
242
+ }
243
+ })
244
+ took(ctx, { s, root, file, found, depend, overrides })
245
+ }
246
+ return { overrides, dependencies, missing }
247
+ }
248
+
249
+ async function clientRoutesModule(ctx, s, p, run) {
250
+ const entry = s.clientEntry()
251
+ const entryFile = entry.startsWith('virtual:') ? join(here, 'client.tsx') : entry
252
+ const levels = [...s.manifest.layouts, ...s.manifest.routes]
253
+ const all = [...new Set(levels.map((r) => s.abs(r.file)))]
254
+ const root = s.root ?? dirname(entryFile)
255
+ const { overrides, dependencies, missing } = await scanOverrides(ctx, s, all, root)
256
+ const files = all.filter(
257
+ (file) =>
258
+ /\.[jt]sx$/.test(file) &&
259
+ (overrides.has(file) || ['generated', 'custom'].includes(skeletonExport(file))),
260
+ )
261
+ const sources = await Promise.all(files.map((file) => entryCss(ctx, file, root)))
262
+ const shared = await entryCss(ctx, entryFile, root)
263
+ if (run.owner && run.owner !== s.shapeEntryOwner)
264
+ throw new Error('entry style scan outlived its server')
265
+ if (run.scan !== s.shapeScanGeneration) throw new Error('entry style scan superseded')
266
+ for (const source of sources) {
267
+ for (const [file, signature] of source.signatures) shared.signatures.set(file, signature)
268
+ for (const file of source.candidates) shared.candidates.add(file)
269
+ for (const file of source.missing) shared.missing.add(file)
270
+ }
271
+ for (const file of missing) {
272
+ shared.candidates.add(file)
273
+ shared.missing.add(file)
274
+ }
275
+ const css = {
276
+ entry: shared.styles,
277
+ levels: new Map(files.map((file, i) => [file, sources[i].styles])),
278
+ overrides,
279
+ build: s.build || null,
280
+ }
281
+ const output = clientRoutes(s.manifest, s.abs, p, css)
282
+ s.shapeEntrySignatures = shared.signatures
283
+ s.shapeEntryCandidates = shared.candidates
284
+ s.shapeEntryMissing = shared.missing
285
+ s.shapeOverrideDependencies = dependencies
286
+ if (s.dev) watchEntryCandidates(s.vite, shared.missing, root)
287
+ return output
288
+ }