@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
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { isAbsolute, join } from 'node:path'
|
|
3
|
+
import { isCSSRequest } from 'vite'
|
|
4
|
+
import { here, ASSETS, SHEET } from './shared.mjs'
|
|
5
|
+
import { parse } from './syntax.mjs'
|
|
6
|
+
import { shapeOverride } from './shape-override.mjs'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `dev`: the stylesheets from `devStyles`; a build asks Nitro's manifest instead (the plugin's expression reads up to
|
|
10
|
+
* the first `)`, which a path can hold). `meta` is read once per level that renders, before the first byte: that's
|
|
11
|
+
* how the entry learns the request's levels (`routeCss`), outermost first, 404 and error pages included.
|
|
12
|
+
*/
|
|
13
|
+
export function serverRoutes(manifest, abs, dev, plan) {
|
|
14
|
+
const files = [
|
|
15
|
+
...manifest.layouts.map((l) => l.file),
|
|
16
|
+
...manifest.routes.map((r) => r.file),
|
|
17
|
+
...manifest.routes.map((r) => r.card).filter(Boolean),
|
|
18
|
+
manifest.defaultCard,
|
|
19
|
+
].filter(Boolean)
|
|
20
|
+
const unique = [...new Set(files)]
|
|
21
|
+
const levels = new Set([...manifest.layouts, ...manifest.routes].map((r) => r.file))
|
|
22
|
+
// A file with no default export renders nothing, and may leave no chunk for the manifest to find.
|
|
23
|
+
const renders = (f) => levels.has(f) && /\bdefault\b/.test(readFileSync(abs(f), 'utf8'))
|
|
24
|
+
const isStatic = (f) => plan?.statics.has(abs(f))
|
|
25
|
+
const clientAssets = (file) =>
|
|
26
|
+
`import.meta.vite.assets(${JSON.stringify({ import: file, environment: 'client' }).replaceAll(')', '\\u0029')})`
|
|
27
|
+
const css = (f) =>
|
|
28
|
+
dev
|
|
29
|
+
? JSON.stringify(dev.files[f] ?? [])
|
|
30
|
+
: `${clientAssets(abs(f) + (isStatic(f) ? ASSETS : ''))}.css`
|
|
31
|
+
return [
|
|
32
|
+
...unique.map((f, i) => `import * as m${i} from ${JSON.stringify(abs(f))}`),
|
|
33
|
+
`import { runtime } from ${JSON.stringify(join(here, 'data.ts'))}`,
|
|
34
|
+
`const matched = new WeakMap()
|
|
35
|
+
const styled = (m, css) => !css.length ? m : { ...m, get meta() {
|
|
36
|
+
const scope = runtime.scope?.()
|
|
37
|
+
if (scope) matched.set(scope, [...(matched.get(scope) ?? []), ...css])
|
|
38
|
+
return m.meta
|
|
39
|
+
} }
|
|
40
|
+
export const routeCss = css => [...new Map([...${JSON.stringify(dev?.entry ?? [])}, ...css, ...(matched.get(runtime.scope?.()) ?? [])].map(c => [c.href, c])).values()]`,
|
|
41
|
+
`export const manifest = ${JSON.stringify({ dir: manifest.dir, routes: manifest.routes, layouts: manifest.layouts, defaultCard: manifest.defaultCard })}`,
|
|
42
|
+
// The built-in 404 and error pages have no state, and stand in only when the app has no route file for them.
|
|
43
|
+
`export const statics = ${JSON.stringify(!plan ? [] : [...[...manifest.layouts, ...manifest.routes].filter((r) => isStatic(r.file)).map((r) => r.id), ...['404', 'error'].filter((id) => !manifest.routes.some((r) => r.id === id))])}`,
|
|
44
|
+
`export const lazy = ${!!plan?.loads.some((l) => l.when !== 'load')}`,
|
|
45
|
+
// The chunks a page's levels and islands will import, for the document's modulepreloads. Dev has no chunks.
|
|
46
|
+
`export const chunks = ${dev ? '{}' : `{ ${[...[...manifest.layouts, ...manifest.routes].filter((r) => !isStatic(r.file) && renders(r.file)).map((r) => [r.id, abs(r.file)]), ...(plan?.islands ?? []).map((i) => [i.id, i.file]), ['$client', join(here, 'run.tsx')]].map(([id, file]) => `${JSON.stringify(id)}: ${clientAssets(file)}`).join(', ')} }`}`,
|
|
47
|
+
`export const modules = { ${unique.map((f, i) => `${JSON.stringify(f)}: ${renders(f) ? `styled(m${i}, ${css(f)})` : `m${i}`}`).join(', ')} }`,
|
|
48
|
+
].join('\n')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The CSS each module reaches in the client graph, as `?assets=client` can't say in dev: the entry's, and each route
|
|
53
|
+
* and layout's. The client env transforms what the browser is about to ask for anyway, and `<link href>` to a module's
|
|
54
|
+
* URL gets the compiled sheet (Tailwind's output too). A module that doesn't transform fails again when it's imported.
|
|
55
|
+
*/
|
|
56
|
+
export async function devStyles(env, manifest, abs, entry) {
|
|
57
|
+
const reachFrom = async (start) => {
|
|
58
|
+
const seen = new Set(),
|
|
59
|
+
css = new Map()
|
|
60
|
+
const walk = async (url) => {
|
|
61
|
+
const mod =
|
|
62
|
+
(await env.moduleGraph.getModuleByUrl(url).catch(() => undefined)) ??
|
|
63
|
+
(await env.moduleGraph.ensureEntryFromUrl(url).catch(() => undefined))
|
|
64
|
+
if (!mod || seen.has(mod) || mod.id?.startsWith('\0virtual:weft/routes')) return // routes import every route
|
|
65
|
+
seen.add(mod)
|
|
66
|
+
await env.transformRequest(mod.url).catch(() => {})
|
|
67
|
+
for (const next of mod.importedModules) {
|
|
68
|
+
if (!next.id) continue
|
|
69
|
+
if (!isCSSRequest(next.id)) await walk(next.url)
|
|
70
|
+
else if (!/[?&](url|inline|raw)\b/.test(next.id))
|
|
71
|
+
css.set(next.url, {
|
|
72
|
+
href: next.url[0] === '/' ? next.url : SHEET + encodeURIComponent(next.id),
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
await walk(start)
|
|
77
|
+
return [...css.values()]
|
|
78
|
+
}
|
|
79
|
+
const files = {}
|
|
80
|
+
for (const { file } of [...manifest.layouts, ...manifest.routes])
|
|
81
|
+
files[file] = await reachFrom(abs(file))
|
|
82
|
+
return {
|
|
83
|
+
entry: await reachFrom(entry.startsWith('virtual:') ? join(here, 'client.tsx') : entry),
|
|
84
|
+
files,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Vite's client, in dev: the fullstack plugin's patch hands a sheet to its `<link data-vite-dev-id>`, so the module
|
|
90
|
+
* doesn't inject a second copy and CSS HMR swaps the link. The server only writes `href`, so the id is added here,
|
|
91
|
+
* before the patched client reads the links.
|
|
92
|
+
*/
|
|
93
|
+
export const markLinks = (
|
|
94
|
+
root,
|
|
95
|
+
) => `document.querySelectorAll('link[rel="stylesheet"]:not([data-vite-dev-id])').forEach(l => {
|
|
96
|
+
const href = l.getAttribute('href')
|
|
97
|
+
if (href.startsWith('${SHEET}')) l.setAttribute('data-vite-dev-id', decodeURIComponent(href.slice(${SHEET.length})))
|
|
98
|
+
else if (href.startsWith('/@fs/')) l.setAttribute('data-vite-dev-id', href.replace(/^\\/@fs(\\/(?=[A-Za-z]:))?/, ''))
|
|
99
|
+
else if (href[0] === '/' && href[1] !== '/') l.setAttribute('data-vite-dev-id', ${JSON.stringify(root)} + href)
|
|
100
|
+
})
|
|
101
|
+
`
|
|
102
|
+
|
|
103
|
+
/** Read a route or entry module only when its parsed syntax is valid. */
|
|
104
|
+
export function shapeBody(file) {
|
|
105
|
+
const result = parse(file, readFileSync(file, 'utf8'))
|
|
106
|
+
if (result.errors.length) {
|
|
107
|
+
const message = result.errors[0]?.message ?? 'invalid source'
|
|
108
|
+
throw new SyntaxError(`${file}: syntax error: ${message}`)
|
|
109
|
+
}
|
|
110
|
+
return result.program.body
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Classify a route skeleton export without evaluating the route. */
|
|
114
|
+
export function skeletonExport(file) {
|
|
115
|
+
const body = shapeBody(file)
|
|
116
|
+
const constants = new Map()
|
|
117
|
+
const bindings = (decl) => {
|
|
118
|
+
if (decl?.type !== 'VariableDeclaration' || decl.kind !== 'const') return
|
|
119
|
+
for (const item of decl.declarations) {
|
|
120
|
+
if (item.id.type !== 'Identifier') continue
|
|
121
|
+
let value = item.init
|
|
122
|
+
while (
|
|
123
|
+
value &&
|
|
124
|
+
[
|
|
125
|
+
'TSAsExpression',
|
|
126
|
+
'TSSatisfiesExpression',
|
|
127
|
+
'TSNonNullExpression',
|
|
128
|
+
'TSTypeAssertion',
|
|
129
|
+
'ParenthesizedExpression',
|
|
130
|
+
].includes(value.type)
|
|
131
|
+
)
|
|
132
|
+
value = value.expression
|
|
133
|
+
constants.set(
|
|
134
|
+
item.id.name,
|
|
135
|
+
value?.value === false && (value.type === 'Literal' || value.type === 'BooleanLiteral'),
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const node of body) bindings(node.declaration ?? node)
|
|
140
|
+
let exported
|
|
141
|
+
for (const node of body) {
|
|
142
|
+
if (node.exportKind === 'type') continue
|
|
143
|
+
if (node.type === 'ExportAllDeclaration') return 'unavailable'
|
|
144
|
+
if (node.type !== 'ExportNamedDeclaration') continue
|
|
145
|
+
const decl = node.declaration
|
|
146
|
+
if (decl?.type === 'VariableDeclaration') {
|
|
147
|
+
if (decl.declarations.some((item) => item.id.type !== 'Identifier')) return 'unavailable'
|
|
148
|
+
if (decl.declarations.some((item) => item.id.name === 'skeleton'))
|
|
149
|
+
exported = decl.kind === 'const' && constants.get('skeleton') ? 'disabled' : 'unavailable'
|
|
150
|
+
} else if (decl?.id?.name === 'skeleton') exported = 'unavailable'
|
|
151
|
+
for (const spec of node.specifiers ?? []) {
|
|
152
|
+
if (spec.exportKind === 'type' || (spec.exported.name ?? spec.exported.value) !== 'skeleton')
|
|
153
|
+
continue
|
|
154
|
+
const local = spec.local.name ?? spec.local.value
|
|
155
|
+
exported = !node.source && constants.get(local) ? 'disabled' : 'unavailable'
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return exported === 'unavailable' && shapeOverride(file) ? 'custom' : (exported ?? 'generated')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Whether a source specifier can name a non-module stylesheet or token sheet. */
|
|
162
|
+
export function shapeStyleSpecifier(spec) {
|
|
163
|
+
return /(?<!\.module)\.css$/.test(spec) || /^weft:tokens\/[\w/-]+$/.test(spec)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Inline CSS is data until a consumer applies it. Other dependencies remain unproven.
|
|
167
|
+
function styleImports(file) {
|
|
168
|
+
return shapeBody(file)
|
|
169
|
+
.filter((node) => node.type === 'ImportDeclaration' && !node.specifiers.length)
|
|
170
|
+
.map((node) => node.source.value)
|
|
171
|
+
.filter(shapeStyleSpecifier)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Resolve a stylesheet or token import without evaluating its importing module. */
|
|
175
|
+
export async function shapeCssSource(ctx, file, spec, onMissing) {
|
|
176
|
+
if (!shapeStyleSpecifier(spec) || /^(https?:|\/\/)/.test(spec)) return
|
|
177
|
+
if (!/^\.{1,2}\//.test(spec) && spec.includes('..')) return
|
|
178
|
+
const found = await ctx.resolve(spec, file, { skipSelf: true })
|
|
179
|
+
if (!found) onMissing?.()
|
|
180
|
+
if (!found?.external && found?.id && isAbsolute(found.id) && found.id.endsWith('.css'))
|
|
181
|
+
return found.id
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Resolve only direct stylesheet and token imports, without evaluating their importing module. */
|
|
185
|
+
export async function shapeCssSources(ctx, file) {
|
|
186
|
+
const styles = []
|
|
187
|
+
for (const spec of styleImports(file)) {
|
|
188
|
+
const style = await shapeCssSource(ctx, file, spec)
|
|
189
|
+
if (style) styles.push(style)
|
|
190
|
+
}
|
|
191
|
+
return [...new Set(styles)]
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** The route facts embedded in the client shape manifest. */
|
|
195
|
+
export function shapeSignature(file) {
|
|
196
|
+
const kind = skeletonExport(file)
|
|
197
|
+
return JSON.stringify([kind, styleImports(file), kind === 'custom' ? shapeOverride(file) : null])
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The routes table's stylesheets and `shapeEntry`. A stylesheet edit in `weft dev` lands here instead of reloading the
|
|
202
|
+
* page: a shape reads its CSS when it's shown.
|
|
203
|
+
*/
|
|
204
|
+
function shapeStyles(files) {
|
|
205
|
+
const requests = files.map((file) => JSON.stringify(file + '?inline'))
|
|
206
|
+
const accept = requests.length
|
|
207
|
+
? `if (import.meta.hot) import.meta.hot.accept([${requests.join(', ')}], (mods) => mods.forEach((m, i) => { if (m) styles[i] = m.default }))\n`
|
|
208
|
+
: ''
|
|
209
|
+
return `${requests.map((request, i) => `import style${i} from ${request}`).join('\n')}
|
|
210
|
+
const styles = [${requests.map((_, i) => `style${i}`).join(', ')}]
|
|
211
|
+
${accept}const shapeEntry = (module, sheets, name, kind) => {
|
|
212
|
+
const component = Reflect.get(module, name)
|
|
213
|
+
const groupCss = Reflect.get(module, '$weftShapeCss')
|
|
214
|
+
const lead = typeof groupCss === 'string' && groupCss ? [groupCss] : []
|
|
215
|
+
const thin = kind === 'generated' && Reflect.get(module, '$weftShapeThin') === true
|
|
216
|
+
return typeof component === 'function'
|
|
217
|
+
? { kind, component, css: null, get cssText() { return [...lead, ...sheets.map((i) => styles[i])] }, cssStatus: 'partial', bytes: null, thin }
|
|
218
|
+
: { kind: 'unavailable', reason: 'missing-shape', css: null, bytes: null }
|
|
219
|
+
}`
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function clientRoutes(manifest, abs, plan, css = {}) {
|
|
223
|
+
const table = manifest.routes
|
|
224
|
+
.filter((r) => r.kind === 'page')
|
|
225
|
+
.map((r) => ({ id: r.id, pattern: r.pattern, layouts: r.layouts }))
|
|
226
|
+
const levels = [...manifest.layouts, ...manifest.routes]
|
|
227
|
+
const isStatic = (r) => plan?.statics.has(abs(r.file))
|
|
228
|
+
const shapeFiles = new Map()
|
|
229
|
+
const styleFiles = new Map()
|
|
230
|
+
const shape = (
|
|
231
|
+
file,
|
|
232
|
+
name = 'default$shape',
|
|
233
|
+
request = file + '?weft-shape',
|
|
234
|
+
kind = 'generated',
|
|
235
|
+
) => {
|
|
236
|
+
if (!shapeFiles.has(request)) shapeFiles.set(request, shapeFiles.size)
|
|
237
|
+
const sources = [...(css.entry ?? []), ...(css.levels?.get(file) ?? [])]
|
|
238
|
+
const styles = [...new Set(sources)].map((style) => {
|
|
239
|
+
if (!styleFiles.has(style)) styleFiles.set(style, styleFiles.size)
|
|
240
|
+
return styleFiles.get(style)
|
|
241
|
+
})
|
|
242
|
+
const args = [JSON.stringify(name), JSON.stringify(kind)].join(', ')
|
|
243
|
+
return `shapeEntry(shape${shapeFiles.get(request)}, [${styles.join(', ')}], ${args})`
|
|
244
|
+
}
|
|
245
|
+
const shapes = levels.map((r) => {
|
|
246
|
+
const file = abs(r.file)
|
|
247
|
+
const classified = skeletonExport(file)
|
|
248
|
+
const kind = classified === 'unavailable' && css.overrides?.has(file) ? 'custom' : classified
|
|
249
|
+
const id = JSON.stringify(r.id)
|
|
250
|
+
if (kind === 'disabled') return `${id}: { kind: 'disabled', css: null, bytes: null }`
|
|
251
|
+
if (kind === 'unavailable')
|
|
252
|
+
return `${id}: { kind: 'unavailable', reason: 'unresolved-export', css: null, bytes: null }`
|
|
253
|
+
if (!/\.[jt]sx$/.test(file))
|
|
254
|
+
return `${id}: { kind: 'unavailable', reason: 'unsupported-file', css: null, bytes: null }`
|
|
255
|
+
if (kind === 'custom') {
|
|
256
|
+
const request = `virtual:weft/shape-override/${encodeURIComponent(file)}.tsx`
|
|
257
|
+
return `${id}: ${shape(file, 'default', request, 'override')}`
|
|
258
|
+
}
|
|
259
|
+
return `${id}: ${shape(file)}`
|
|
260
|
+
})
|
|
261
|
+
for (const [id, name] of [
|
|
262
|
+
['404', 'NotFoundPage$shape'],
|
|
263
|
+
['error', 'ErrorPage$shape'],
|
|
264
|
+
]) {
|
|
265
|
+
if (!manifest.routes.some((route) => route.id === id))
|
|
266
|
+
shapes.push(`${JSON.stringify(id)}: ${shape(join(here, 'builtin.tsx'), name)}`)
|
|
267
|
+
}
|
|
268
|
+
const shapeImports = [...shapeFiles].map(
|
|
269
|
+
([request, i]) => `import * as shape${i} from ${JSON.stringify(request)}`,
|
|
270
|
+
)
|
|
271
|
+
// A static route loads only what its module imports that isn't code: its stylesheets, for a navigation to it.
|
|
272
|
+
const loads = levels.map(
|
|
273
|
+
(r) =>
|
|
274
|
+
`${JSON.stringify(r.id)}: () => import(${JSON.stringify(abs(r.file) + (isStatic(r) ? ASSETS : ''))})`,
|
|
275
|
+
)
|
|
276
|
+
// Straight from the file: a module of its own per island would be a chunk that only re-exports.
|
|
277
|
+
const islands = (plan?.islands ?? []).map(
|
|
278
|
+
(i) =>
|
|
279
|
+
`${JSON.stringify(i.id)}: () => import(${JSON.stringify(i.file)}).then(({ $i${i.index}: d }) => ({ default: d }))`,
|
|
280
|
+
)
|
|
281
|
+
return `${shapeImports.join('\n')}
|
|
282
|
+
${shapeStyles([...styleFiles.keys()])}
|
|
283
|
+
export const shapes = { ${shapes.join(', ')} }
|
|
284
|
+
export const build = ${JSON.stringify(css.build ?? null)}
|
|
285
|
+
export const table = ${JSON.stringify(table)}
|
|
286
|
+
export const load = { ${loads.join(', ')} }
|
|
287
|
+
export const islands = { ${islands.join(', ')} }
|
|
288
|
+
`
|
|
289
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { readdirSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { parse, valueNames, pruneImports } from './syntax.mjs'
|
|
5
|
+
|
|
6
|
+
// ---- server modules (research 27 §2) ------------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
/** `src/server/**` and `*.server.ts(x)`: files whose `query`/`mutation` exports become client stubs. */
|
|
9
|
+
export const isServerModule = (rel) =>
|
|
10
|
+
rel.startsWith('src/server/') || /\.server\.[cm]?[jt]sx?$/.test(rel)
|
|
11
|
+
|
|
12
|
+
export function scanServerModules(root) {
|
|
13
|
+
const out = []
|
|
14
|
+
const walk = (dir) => {
|
|
15
|
+
let list
|
|
16
|
+
try {
|
|
17
|
+
list = readdirSync(join(root, dir), { withFileTypes: true })
|
|
18
|
+
} catch {
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
for (const d of list) {
|
|
22
|
+
const rel = `${dir}/${d.name}`
|
|
23
|
+
if (d.isDirectory()) {
|
|
24
|
+
if (d.name !== 'node_modules' && !d.name.startsWith('.')) walk(rel)
|
|
25
|
+
} else if (
|
|
26
|
+
/\.[cm]?[jt]sx?$/.test(d.name) &&
|
|
27
|
+
!/\.(test|spec)\./.test(d.name) &&
|
|
28
|
+
!d.name.endsWith('.d.ts') &&
|
|
29
|
+
isServerModule(rel)
|
|
30
|
+
)
|
|
31
|
+
out.push(rel)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
walk('src')
|
|
35
|
+
return out.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const FN_KINDS = new Set(['query', 'mutation', 'stream', 'topic', 'channel', 'sharedState'])
|
|
39
|
+
|
|
40
|
+
/** Local names bound to the `FN_KINDS` from @weft-js/start. */
|
|
41
|
+
function fnLocals(ast) {
|
|
42
|
+
const locals = new Map()
|
|
43
|
+
for (const n of ast.program.body) {
|
|
44
|
+
if (
|
|
45
|
+
n.type !== 'ImportDeclaration' ||
|
|
46
|
+
n.importKind === 'type' ||
|
|
47
|
+
!/^@weft-js\/start(\/data)?$/.test(n.source.value)
|
|
48
|
+
)
|
|
49
|
+
continue
|
|
50
|
+
for (const s of n.specifiers)
|
|
51
|
+
if (
|
|
52
|
+
s.type === 'ImportSpecifier' &&
|
|
53
|
+
s.importKind !== 'type' &&
|
|
54
|
+
FN_KINDS.has(s.imported.name ?? s.imported.value)
|
|
55
|
+
)
|
|
56
|
+
locals.set(s.local.name, s.imported.name ?? s.imported.value)
|
|
57
|
+
}
|
|
58
|
+
return locals
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** `export const name = query({ … })` statements: the call, its options object and the export name. */
|
|
62
|
+
function fnExports(ast, locals) {
|
|
63
|
+
const out = []
|
|
64
|
+
for (const n of ast.program.body) {
|
|
65
|
+
if (n.type !== 'ExportNamedDeclaration' || n.declaration?.type !== 'VariableDeclaration')
|
|
66
|
+
continue
|
|
67
|
+
for (const d of n.declaration.declarations) {
|
|
68
|
+
const call =
|
|
69
|
+
d.init?.type === 'CallExpression'
|
|
70
|
+
? d.init
|
|
71
|
+
: d.init?.type === 'TSAsExpression' || d.init?.type === 'TSSatisfiesExpression'
|
|
72
|
+
? d.init.expression
|
|
73
|
+
: undefined
|
|
74
|
+
if (
|
|
75
|
+
call?.type !== 'CallExpression' ||
|
|
76
|
+
call.callee.type !== 'Identifier' ||
|
|
77
|
+
!locals.has(call.callee.name)
|
|
78
|
+
)
|
|
79
|
+
continue
|
|
80
|
+
const obj = call.arguments[locals.get(call.callee.name) === 'sharedState' ? 1 : 0] // `sharedState(name, { … })`
|
|
81
|
+
if (obj?.type !== 'ObjectExpression' || d.id.type !== 'Identifier') continue
|
|
82
|
+
out.push({
|
|
83
|
+
node: n,
|
|
84
|
+
name: d.id.name,
|
|
85
|
+
callee: call.callee.name,
|
|
86
|
+
obj,
|
|
87
|
+
single: n.declaration.declarations.length === 1,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return out
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const prop = (obj, key) =>
|
|
95
|
+
obj.properties.find(
|
|
96
|
+
(p) => p.type === 'Property' && !p.computed && (p.key.name ?? p.key.value) === key,
|
|
97
|
+
)
|
|
98
|
+
/** A function's id: its name and a hash of its file, stable across builds. */
|
|
99
|
+
export const fnId = (name, rel) =>
|
|
100
|
+
`${name}.${createHash('sha256').update(rel).digest('hex').slice(0, 6)}`
|
|
101
|
+
|
|
102
|
+
/** The server build of a server module: each definition gets its `$id`. Exported for tests. */
|
|
103
|
+
export function withIds(code, file, rel) {
|
|
104
|
+
const ast = parse(file, code)
|
|
105
|
+
const locals = fnLocals(ast)
|
|
106
|
+
const all = fnExports(ast, locals)
|
|
107
|
+
for (const f of all) {
|
|
108
|
+
const kind = locals.get(f.callee)
|
|
109
|
+
if ((kind === 'channel' || kind === 'sharedState') && !prop(f.obj, 'authorize')) {
|
|
110
|
+
const line = code.slice(0, f.obj.start).split('\n').length
|
|
111
|
+
throw Object.assign(
|
|
112
|
+
new Error(
|
|
113
|
+
`R001: ${rel}:${line} \`${f.name}\` has no \`authorize\`: anyone could join it.\n fix: add \`authorize\` (\`() => true\` must be written out)`,
|
|
114
|
+
),
|
|
115
|
+
{ code: 'R001' },
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const list = all.filter((f) => !prop(f.obj, '$id'))
|
|
120
|
+
let out = code
|
|
121
|
+
for (const f of list.sort((a, b) => b.obj.start - a.obj.start))
|
|
122
|
+
out =
|
|
123
|
+
out.slice(0, f.obj.start + 1) +
|
|
124
|
+
` $id: ${JSON.stringify(fnId(f.name, rel))},` +
|
|
125
|
+
out.slice(f.obj.start + 1)
|
|
126
|
+
return out
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The client build of a server module: stubs (`query({ $id, shape })`, `mutation({ $id, optimistic })`), the top-level
|
|
131
|
+
* declarations a kept `optimistic` reads as values, and the imports those use. Nothing else: no `run`, no database
|
|
132
|
+
* client, no other export, and no types (they're erased, and `z.infer<typeof input>` would keep `input` and zod).
|
|
133
|
+
* Exported for tests.
|
|
134
|
+
*/
|
|
135
|
+
export function clientStubs(code, file, rel, shapes) {
|
|
136
|
+
const ast = parse(file, code)
|
|
137
|
+
const locals = fnLocals(ast)
|
|
138
|
+
const defs = new Map(fnExports(ast, locals).map((f) => [f.node, f]))
|
|
139
|
+
const kept = [] // [start, text]
|
|
140
|
+
const candidates = []
|
|
141
|
+
const names = (text) => new Set(text.match(/[A-Za-z_$][\w$]*/g) ?? [])
|
|
142
|
+
for (const n of ast.program.body) {
|
|
143
|
+
if (n.type === 'ImportDeclaration') {
|
|
144
|
+
if (n.specifiers.length) kept.push([n.start, code.slice(n.start, n.end)]) // pruned below; side-effect imports go
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
const f = defs.get(n)
|
|
148
|
+
if (f && f.single) {
|
|
149
|
+
const opt = prop(f.obj, 'optimistic')
|
|
150
|
+
const id = fnId(f.name, rel)
|
|
151
|
+
const kind = locals.get(f.callee)
|
|
152
|
+
if (kind === 'channel' || kind === 'sharedState') {
|
|
153
|
+
const initial = prop(f.obj, 'initial')
|
|
154
|
+
kept.push([
|
|
155
|
+
n.start,
|
|
156
|
+
`export const ${f.name} = ${f.callee}(${kind === 'sharedState' ? "'', " : ''}{ $id: ${JSON.stringify(id)}, $stub: 1${initial ? `, ${code.slice(initial.start, initial.end)}` : ''} })`,
|
|
157
|
+
])
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
// The tree the schema gave when there's a table; else what was declared, as it was written.
|
|
161
|
+
const shape =
|
|
162
|
+
shapes?.[id] !== undefined
|
|
163
|
+
? `shape: ${JSON.stringify(shapes[id])}`
|
|
164
|
+
: prop(f.obj, 'shape') && code.slice(prop(f.obj, 'shape').start, prop(f.obj, 'shape').end)
|
|
165
|
+
kept.push([
|
|
166
|
+
n.start,
|
|
167
|
+
`export const ${f.name} = ${f.callee}({ $id: ${JSON.stringify(id)}${opt ? `, ${code.slice(opt.start, opt.end)}` : ''}${shape ? `, ${shape}` : ''} })`,
|
|
168
|
+
])
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
const decl = n.type === 'ExportNamedDeclaration' ? n.declaration : n
|
|
172
|
+
if (!decl) continue
|
|
173
|
+
if (/^TS(TypeAlias|Interface)Declaration$/.test(decl.type) || n.exportKind === 'type') continue
|
|
174
|
+
if (/^TS(Enum|Module)Declaration$/.test(decl.type)) {
|
|
175
|
+
// the kinds with code in them, kept like any helper
|
|
176
|
+
candidates.push({ n, declares: [decl.id.name] })
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
const bound =
|
|
180
|
+
decl.type === 'VariableDeclaration'
|
|
181
|
+
? decl.declarations.flatMap((d) => [...names(code.slice(d.id.start, d.id.end))])
|
|
182
|
+
: decl.id
|
|
183
|
+
? [decl.id.name]
|
|
184
|
+
: []
|
|
185
|
+
if (bound.length) candidates.push({ n, declares: bound })
|
|
186
|
+
}
|
|
187
|
+
// Helpers a kept `optimistic` reads (and what they read), in source order.
|
|
188
|
+
for (let grew = true; grew;) {
|
|
189
|
+
grew = false
|
|
190
|
+
const used = valueNames(parse(file, kept.map(([, t]) => t).join('\n')))
|
|
191
|
+
for (const c of candidates.splice(0)) {
|
|
192
|
+
if (c.declares.some((d) => used.has(d))) {
|
|
193
|
+
kept.push([c.n.start, code.slice(c.n.start, c.n.end)])
|
|
194
|
+
grew = true
|
|
195
|
+
} else candidates.push(c)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const out =
|
|
199
|
+
`// weft: client stubs for ${rel} (research 27 §2)\n` +
|
|
200
|
+
kept
|
|
201
|
+
.sort((a, b) => a[0] - b[0])
|
|
202
|
+
.map(([, t]) => t)
|
|
203
|
+
.join('\n') +
|
|
204
|
+
'\n'
|
|
205
|
+
return pruneImports(out, file)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Q003: a `query`/`mutation` defined in a file that isn't a server module (its name and kind), or undefined. */
|
|
209
|
+
export function definesFns(code, file) {
|
|
210
|
+
if (
|
|
211
|
+
!/@weft-js\/start/.test(code) ||
|
|
212
|
+
!/\b(query|mutation|stream|topic|channel|sharedState)\b/.test(code)
|
|
213
|
+
)
|
|
214
|
+
return
|
|
215
|
+
const ast = parse(file, code)
|
|
216
|
+
const locals = fnLocals(ast)
|
|
217
|
+
if (!locals.size) return
|
|
218
|
+
const f = fnExports(ast, locals)[0]
|
|
219
|
+
if (f) return { name: f.name, kind: locals.get(f.callee) }
|
|
220
|
+
for (const [local, kind] of locals)
|
|
221
|
+
if (new RegExp(`(?<![\\w$.])${local}\\s*\\(`).test(code)) return { name: local, kind }
|
|
222
|
+
}
|