@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,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
+ }