@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.
- package/README.md +36 -3
- package/crate/Cargo.lock +1347 -0
- package/crate/Cargo.toml +72 -0
- package/crate/rust-toolchain.toml +3 -0
- package/crate/src/build/emit.rs +103 -0
- package/crate/src/build.rs +367 -0
- package/crate/src/ctx.rs +186 -0
- package/crate/src/envelope.rs +163 -0
- package/crate/src/functions.rs +173 -0
- package/crate/src/lib.rs +118 -0
- package/crate/src/native.rs +212 -0
- package/crate/src/otel.rs +62 -0
- package/crate/src/server.rs +203 -0
- package/package.json +46 -37
- package/src/app.mjs +38 -8
- package/src/asset-receipt.d.mts +13 -0
- package/src/asset-receipt.mjs +61 -0
- package/src/auth/better-auth.ts +215 -17
- package/src/auth/drizzle.ts +1 -1
- package/src/auth.ts +21 -6
- package/src/boot.ts +19 -8
- package/src/bus/postgres.ts +32 -19
- package/src/bus.ts +7 -4
- package/src/{capture.mjs → capture.ts} +34 -1
- package/src/changes-sql.d.mts +8 -2
- package/src/changes-sql.mjs +26 -10
- package/src/changes.ts +165 -66
- package/src/channel/connections.ts +216 -0
- package/src/channel/socket.ts +396 -0
- package/src/channel-fake.ts +19 -4
- package/src/channel.ts +120 -492
- package/src/client.tsx +9 -2
- package/src/contract/describe.mjs +231 -0
- package/src/contract.mjs +144 -157
- package/src/data/cache.ts +354 -0
- package/src/data/definitions.ts +94 -0
- package/src/data/draft.ts +104 -0
- package/src/data/foundation.ts +95 -0
- package/src/data/live-api.ts +22 -0
- package/src/data/mutation.ts +138 -0
- package/src/data/primitives.ts +135 -0
- package/src/data/query.ts +106 -0
- package/src/data/transport.ts +121 -0
- package/src/data/types.ts +303 -0
- package/src/data.ts +69 -894
- package/src/db/bun-sqlite.d.ts +4 -1
- package/src/db/changes-mysql.ts +61 -21
- package/src/db/changes-postgres.ts +197 -118
- package/src/db/changes-sqlite.ts +131 -64
- package/src/db/core.ts +38 -13
- package/src/db/index.ts +20 -4
- package/src/db/mysql.ts +22 -6
- package/src/db/postgres.ts +49 -13
- package/src/db/sqlite.ts +53 -27
- package/src/derive.d.mts +4 -1
- package/src/derive.mjs +136 -39
- package/src/dev-runtime.mjs +8 -4
- package/src/exec/errors.ts +57 -0
- package/src/exec/landing.ts +66 -0
- package/src/exec/scope.ts +174 -0
- package/src/exec/touch.ts +80 -0
- package/src/exec/validation.ts +176 -0
- package/src/exec.ts +191 -341
- package/src/extension.ts +8 -2
- package/src/first-paint.ts +26 -9
- package/src/form/value.ts +15 -0
- package/src/form.tsx +132 -43
- package/src/hub/channels.ts +102 -0
- package/src/hub/connections.ts +147 -0
- package/src/hub/delivery.ts +62 -0
- package/src/hub/feed.ts +168 -0
- package/src/hub/presence.ts +64 -0
- package/src/hub/route.ts +158 -0
- package/src/hub/subscriptions.ts +212 -0
- package/src/hub/topic.ts +98 -0
- package/src/hub/tracked.ts +12 -0
- package/src/hub/types.ts +190 -0
- package/src/hub/view.ts +147 -0
- package/src/hub.ts +132 -758
- package/src/index.ts +84 -10
- package/src/inline.mjs +356 -0
- package/src/inspect/compare.mjs +162 -0
- package/src/inspect/routes.mjs +154 -0
- package/src/inspect/samples.mjs +59 -0
- package/src/inspect/url.mjs +2 -0
- package/src/inspect.mjs +146 -187
- package/src/link/election.ts +178 -0
- package/src/link/lines.ts +27 -0
- package/src/link.ts +90 -166
- package/src/list-drizzle.ts +104 -18
- package/src/list.ts +127 -36
- package/src/listen.ts +32 -6
- package/src/live/api.ts +18 -0
- package/src/live/backoff.ts +6 -0
- package/src/live/frames.ts +42 -0
- package/src/live/read.ts +77 -0
- package/src/live/sub-types.ts +24 -0
- package/src/live/transport.ts +86 -0
- package/src/live/types.ts +52 -0
- package/src/live-fake.ts +20 -5
- package/src/live-target.mjs +112 -37
- package/src/live.ts +116 -225
- package/src/media.ts +8 -1
- package/src/migrations.mjs +14 -6
- package/src/mux.ts +161 -64
- package/src/og/DefaultCard.tsx +40 -9
- package/src/og/fonts.mjs +16 -8
- package/src/og/images.mjs +43 -22
- package/src/og/index.mjs +119 -43
- package/src/og/native.mjs +11 -4
- package/src/range.mjs +15 -3
- package/src/router.ts +56 -6
- package/src/routes.mjs +73 -22
- package/src/run/admission.ts +80 -0
- package/src/run/boot.tsx +115 -0
- package/src/run/cache-presentation.ts +67 -0
- package/src/run/cache.ts +254 -0
- package/src/run/helpers.ts +197 -0
- package/src/run/listeners.ts +155 -0
- package/src/run/navigation.ts +382 -0
- package/src/run/page-skeleton.tsx +35 -0
- package/src/run/prefetch.ts +128 -0
- package/src/run/retain-view.ts +18 -0
- package/src/run/shape-preparation.ts +97 -0
- package/src/run/shape-styles.ts +72 -0
- package/src/run/source-context/pin.ts +124 -0
- package/src/run/source-context.ts +311 -0
- package/src/run.tsx +50 -178
- package/src/rust/common.ts +163 -0
- package/src/rust/native.ts +282 -0
- package/src/rust.ts +103 -229
- package/src/server/card.tsx +65 -0
- package/src/server/compression.ts +115 -0
- package/src/server/core.ts +70 -0
- package/src/server/dispatch.ts +70 -0
- package/src/server/form.ts +85 -0
- package/src/server/functions.ts +129 -0
- package/src/server/handler.ts +39 -0
- package/src/server/head.ts +96 -0
- package/src/server/media.ts +60 -0
- package/src/server/navigation-cache.ts +80 -0
- package/src/server/navigation.tsx +85 -0
- package/src/server/page-cold.ts +95 -0
- package/src/server/page-stream.tsx +181 -0
- package/src/server/page-types.ts +51 -0
- package/src/server/page-warm.ts +130 -0
- package/src/server/page.ts +69 -0
- package/src/server/payload.ts +89 -0
- package/src/server/prerender.ts +60 -0
- package/src/server/preview.tsx +67 -0
- package/src/server/render.ts +167 -0
- package/src/server/request.ts +98 -0
- package/src/server/resolver.ts +228 -0
- package/src/server/routes.ts +150 -0
- package/src/server/services.ts +69 -0
- package/src/server/text.ts +29 -0
- package/src/server/types.ts +160 -0
- package/src/server.tsx +82 -971
- package/src/shape.mjs +30 -6
- package/src/share/display.ts +50 -0
- package/src/share/network.ts +119 -0
- package/src/share/receive.ts +70 -0
- package/src/share/state.ts +30 -0
- package/src/share/tab-queue.ts +28 -0
- package/src/share-server.ts +128 -43
- package/src/share.ts +103 -209
- package/src/sheets.mjs +30 -18
- package/src/sidecar/build.mjs +170 -0
- package/src/sidecar/cargo.mjs +164 -0
- package/src/sidecar/common.mjs +17 -0
- package/src/sidecar/dev-build.mjs +91 -0
- package/src/sidecar/dev-watch.mjs +88 -0
- package/src/sidecar/manifest.mjs +19 -0
- package/src/sidecar/process.mjs +58 -0
- package/src/sidecar/refresh.mjs +19 -0
- package/src/sidecar.mjs +75 -416
- package/src/socket/check.ts +50 -0
- package/src/socket/deliver.ts +16 -0
- package/src/socket/publish.ts +56 -0
- package/src/socket/rooms.ts +41 -0
- package/src/socket/route.ts +79 -0
- package/src/socket/shared.ts +26 -0
- package/src/socket/types.ts +48 -0
- package/src/socket.ts +205 -194
- package/src/stream.ts +10 -1
- package/src/verify/app.ts +39 -0
- package/src/verify/cleanup.ts +27 -0
- package/src/verify/database.ts +11 -0
- package/src/verify/live.ts +63 -0
- package/src/verify/notify.ts +35 -0
- package/src/verify/skip.ts +19 -0
- package/src/verify/support.ts +149 -0
- package/src/verify/timing.ts +15 -0
- package/src/verify/trigger.ts +14 -0
- package/src/verify.ts +77 -176
- package/src/view.tsx +96 -11
- package/src/vite/asset-bytes.mjs +95 -0
- package/src/vite/assets.mjs +117 -0
- package/src/vite/config-hooks.mjs +166 -0
- package/src/vite/dev-hooks.mjs +103 -0
- package/src/vite/dev.mjs +102 -0
- package/src/vite/entries.mjs +145 -0
- package/src/vite/module-hooks.mjs +288 -0
- package/src/vite/route-modules.mjs +289 -0
- package/src/vite/server-functions.mjs +222 -0
- package/src/vite/shape-entry-css.mjs +187 -0
- package/src/vite/shape-override.mjs +226 -0
- package/src/vite/shared.mjs +51 -0
- package/src/vite/sheets.mjs +92 -0
- package/src/vite/state.mjs +89 -0
- package/src/vite/syntax.mjs +139 -0
- package/src/vite.mjs +152 -853
- package/src/capture.d.mts +0 -1
package/src/vite/dev.mjs
ADDED
|
@@ -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
|
+
}
|