@weft-js/start 0.1.1 → 0.1.3

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 (214) hide show
  1. package/README.md +1278 -12
  2. package/crate/Cargo.lock +1347 -0
  3. package/crate/Cargo.toml +72 -0
  4. package/crate/README.md +322 -0
  5. package/crate/rust-toolchain.toml +3 -0
  6. package/crate/src/build/emit.rs +103 -0
  7. package/crate/src/build.rs +367 -0
  8. package/crate/src/ctx.rs +186 -0
  9. package/crate/src/envelope.rs +163 -0
  10. package/crate/src/functions.rs +173 -0
  11. package/crate/src/lib.rs +118 -0
  12. package/crate/src/native.rs +212 -0
  13. package/crate/src/otel.rs +62 -0
  14. package/crate/src/server.rs +203 -0
  15. package/package.json +46 -37
  16. package/src/app.mjs +38 -8
  17. package/src/asset-receipt.d.mts +13 -0
  18. package/src/asset-receipt.mjs +61 -0
  19. package/src/auth/better-auth.ts +215 -17
  20. package/src/auth/drizzle.ts +1 -1
  21. package/src/auth.ts +21 -6
  22. package/src/boot.ts +19 -8
  23. package/src/bus/postgres.ts +32 -19
  24. package/src/bus.ts +7 -4
  25. package/src/{capture.mjs → capture.ts} +34 -1
  26. package/src/changes-sql.d.mts +8 -2
  27. package/src/changes-sql.mjs +26 -10
  28. package/src/changes.ts +165 -66
  29. package/src/channel/connections.ts +216 -0
  30. package/src/channel/socket.ts +396 -0
  31. package/src/channel-fake.ts +19 -4
  32. package/src/channel.ts +120 -492
  33. package/src/client.tsx +9 -2
  34. package/src/contract/describe.mjs +231 -0
  35. package/src/contract.mjs +144 -157
  36. package/src/data/cache.ts +354 -0
  37. package/src/data/definitions.ts +94 -0
  38. package/src/data/draft.ts +104 -0
  39. package/src/data/foundation.ts +95 -0
  40. package/src/data/live-api.ts +22 -0
  41. package/src/data/mutation.ts +138 -0
  42. package/src/data/primitives.ts +135 -0
  43. package/src/data/query.ts +106 -0
  44. package/src/data/transport.ts +121 -0
  45. package/src/data/types.ts +303 -0
  46. package/src/data.ts +69 -894
  47. package/src/db/bun-sqlite.d.ts +4 -1
  48. package/src/db/changes-mysql.ts +61 -21
  49. package/src/db/changes-postgres.ts +197 -118
  50. package/src/db/changes-sqlite.ts +131 -64
  51. package/src/db/core.ts +38 -13
  52. package/src/db/index.ts +20 -4
  53. package/src/db/mysql.ts +22 -6
  54. package/src/db/postgres.ts +49 -13
  55. package/src/db/sqlite.ts +53 -27
  56. package/src/derive.d.mts +4 -1
  57. package/src/derive.mjs +136 -39
  58. package/src/dev-runtime.mjs +8 -4
  59. package/src/exec/errors.ts +57 -0
  60. package/src/exec/landing.ts +66 -0
  61. package/src/exec/scope.ts +174 -0
  62. package/src/exec/touch.ts +80 -0
  63. package/src/exec/validation.ts +176 -0
  64. package/src/exec.ts +191 -341
  65. package/src/extension.ts +8 -2
  66. package/src/first-paint.ts +26 -9
  67. package/src/form/value.ts +15 -0
  68. package/src/form.tsx +132 -43
  69. package/src/hub/channels.ts +102 -0
  70. package/src/hub/connections.ts +147 -0
  71. package/src/hub/delivery.ts +62 -0
  72. package/src/hub/feed.ts +168 -0
  73. package/src/hub/presence.ts +64 -0
  74. package/src/hub/route.ts +158 -0
  75. package/src/hub/subscriptions.ts +212 -0
  76. package/src/hub/topic.ts +98 -0
  77. package/src/hub/tracked.ts +12 -0
  78. package/src/hub/types.ts +190 -0
  79. package/src/hub/view.ts +147 -0
  80. package/src/hub.ts +132 -758
  81. package/src/index.ts +84 -10
  82. package/src/inline.mjs +356 -0
  83. package/src/inspect/compare.mjs +162 -0
  84. package/src/inspect/routes.mjs +154 -0
  85. package/src/inspect/samples.mjs +59 -0
  86. package/src/inspect/url.mjs +2 -0
  87. package/src/inspect.mjs +146 -187
  88. package/src/link/election.ts +178 -0
  89. package/src/link/lines.ts +27 -0
  90. package/src/link.ts +90 -166
  91. package/src/list-drizzle.ts +104 -18
  92. package/src/list.ts +127 -36
  93. package/src/listen.ts +32 -6
  94. package/src/live/api.ts +18 -0
  95. package/src/live/backoff.ts +6 -0
  96. package/src/live/frames.ts +42 -0
  97. package/src/live/read.ts +77 -0
  98. package/src/live/sub-types.ts +24 -0
  99. package/src/live/transport.ts +86 -0
  100. package/src/live/types.ts +52 -0
  101. package/src/live-fake.ts +20 -5
  102. package/src/live-target.mjs +112 -37
  103. package/src/live.ts +116 -225
  104. package/src/media.ts +8 -1
  105. package/src/migrations.mjs +14 -6
  106. package/src/mux.ts +161 -64
  107. package/src/og/DefaultCard.tsx +40 -9
  108. package/src/og/fonts.mjs +16 -8
  109. package/src/og/images.mjs +43 -22
  110. package/src/og/index.mjs +119 -43
  111. package/src/og/native.mjs +11 -4
  112. package/src/range.mjs +15 -3
  113. package/src/router.ts +56 -6
  114. package/src/routes.mjs +73 -22
  115. package/src/run/admission.ts +80 -0
  116. package/src/run/boot.tsx +115 -0
  117. package/src/run/cache-presentation.ts +67 -0
  118. package/src/run/cache.ts +254 -0
  119. package/src/run/helpers.ts +197 -0
  120. package/src/run/listeners.ts +155 -0
  121. package/src/run/navigation.ts +382 -0
  122. package/src/run/page-skeleton.tsx +35 -0
  123. package/src/run/prefetch.ts +128 -0
  124. package/src/run/retain-view.ts +18 -0
  125. package/src/run/shape-preparation.ts +97 -0
  126. package/src/run/shape-styles.ts +72 -0
  127. package/src/run/source-context/pin.ts +124 -0
  128. package/src/run/source-context.ts +311 -0
  129. package/src/run.tsx +50 -178
  130. package/src/rust/common.ts +163 -0
  131. package/src/rust/native.ts +282 -0
  132. package/src/rust.ts +103 -229
  133. package/src/server/card.tsx +65 -0
  134. package/src/server/compression.ts +115 -0
  135. package/src/server/core.ts +70 -0
  136. package/src/server/dispatch.ts +70 -0
  137. package/src/server/form.ts +85 -0
  138. package/src/server/functions.ts +129 -0
  139. package/src/server/handler.ts +39 -0
  140. package/src/server/head.ts +96 -0
  141. package/src/server/media.ts +60 -0
  142. package/src/server/navigation-cache.ts +80 -0
  143. package/src/server/navigation.tsx +85 -0
  144. package/src/server/page-cold.ts +95 -0
  145. package/src/server/page-stream.tsx +181 -0
  146. package/src/server/page-types.ts +51 -0
  147. package/src/server/page-warm.ts +130 -0
  148. package/src/server/page.ts +69 -0
  149. package/src/server/payload.ts +89 -0
  150. package/src/server/prerender.ts +60 -0
  151. package/src/server/preview.tsx +67 -0
  152. package/src/server/render.ts +167 -0
  153. package/src/server/request.ts +98 -0
  154. package/src/server/resolver.ts +228 -0
  155. package/src/server/routes.ts +150 -0
  156. package/src/server/services.ts +69 -0
  157. package/src/server/text.ts +29 -0
  158. package/src/server/types.ts +160 -0
  159. package/src/server.tsx +82 -971
  160. package/src/shape.mjs +30 -6
  161. package/src/share/display.ts +50 -0
  162. package/src/share/network.ts +119 -0
  163. package/src/share/receive.ts +70 -0
  164. package/src/share/state.ts +30 -0
  165. package/src/share/tab-queue.ts +28 -0
  166. package/src/share-server.ts +128 -43
  167. package/src/share.ts +103 -209
  168. package/src/sheets.mjs +30 -18
  169. package/src/sidecar/build.mjs +170 -0
  170. package/src/sidecar/cargo.mjs +164 -0
  171. package/src/sidecar/common.mjs +17 -0
  172. package/src/sidecar/dev-build.mjs +91 -0
  173. package/src/sidecar/dev-watch.mjs +88 -0
  174. package/src/sidecar/manifest.mjs +19 -0
  175. package/src/sidecar/process.mjs +58 -0
  176. package/src/sidecar/refresh.mjs +19 -0
  177. package/src/sidecar.mjs +75 -416
  178. package/src/socket/check.ts +50 -0
  179. package/src/socket/deliver.ts +16 -0
  180. package/src/socket/publish.ts +56 -0
  181. package/src/socket/rooms.ts +41 -0
  182. package/src/socket/route.ts +79 -0
  183. package/src/socket/shared.ts +26 -0
  184. package/src/socket/types.ts +48 -0
  185. package/src/socket.ts +205 -194
  186. package/src/stream.ts +10 -1
  187. package/src/verify/app.ts +39 -0
  188. package/src/verify/cleanup.ts +27 -0
  189. package/src/verify/database.ts +11 -0
  190. package/src/verify/live.ts +63 -0
  191. package/src/verify/notify.ts +35 -0
  192. package/src/verify/skip.ts +19 -0
  193. package/src/verify/support.ts +149 -0
  194. package/src/verify/timing.ts +15 -0
  195. package/src/verify/trigger.ts +14 -0
  196. package/src/verify.ts +77 -176
  197. package/src/view.tsx +96 -11
  198. package/src/vite/asset-bytes.mjs +95 -0
  199. package/src/vite/assets.mjs +117 -0
  200. package/src/vite/config-hooks.mjs +166 -0
  201. package/src/vite/dev-hooks.mjs +103 -0
  202. package/src/vite/dev.mjs +102 -0
  203. package/src/vite/entries.mjs +145 -0
  204. package/src/vite/module-hooks.mjs +288 -0
  205. package/src/vite/route-modules.mjs +289 -0
  206. package/src/vite/server-functions.mjs +222 -0
  207. package/src/vite/shape-entry-css.mjs +187 -0
  208. package/src/vite/shape-override.mjs +226 -0
  209. package/src/vite/shared.mjs +51 -0
  210. package/src/vite/sheets.mjs +92 -0
  211. package/src/vite/state.mjs +89 -0
  212. package/src/vite/syntax.mjs +139 -0
  213. package/src/vite.mjs +152 -853
  214. package/src/capture.d.mts +0 -1
@@ -0,0 +1,62 @@
1
+ //! With the `otel` feature, a call's span continues the trace TS started: the app installs an OpenTelemetry layer, and
2
+ //! the envelope's `traceparent` becomes the parent. Without one, `trace_id` and `parent_id` are plain span fields.
3
+
4
+ use opentelemetry::Context;
5
+ use opentelemetry::trace::{SpanContext, SpanId, TraceContextExt, TraceFlags, TraceId, TraceState};
6
+ use tracing_opentelemetry::OpenTelemetrySpanExt;
7
+
8
+ pub(crate) fn parent(traceparent: &str) -> Option<SpanContext> {
9
+ let mut parts = traceparent.split('-');
10
+ let (version, trace, span, flags) =
11
+ (parts.next()?, parts.next()?, parts.next()?, parts.next()?);
12
+ if version != "00" {
13
+ return None;
14
+ }
15
+ let trace = TraceId::from_hex(trace)
16
+ .ok()
17
+ .filter(|t| *t != TraceId::INVALID)?;
18
+ let span = SpanId::from_hex(span)
19
+ .ok()
20
+ .filter(|s| *s != SpanId::INVALID)?;
21
+ let flags = TraceFlags::new(u8::from_str_radix(flags, 16).ok()?);
22
+ Some(SpanContext::new(
23
+ trace,
24
+ span,
25
+ flags,
26
+ true,
27
+ TraceState::default(),
28
+ ))
29
+ }
30
+
31
+ pub(crate) fn link(span: &tracing::Span, traceparent: Option<&str>) {
32
+ if let Some(remote) = traceparent.and_then(parent) {
33
+ let _ = span.set_parent(Context::new().with_remote_span_context(remote));
34
+ }
35
+ }
36
+
37
+ #[cfg(test)]
38
+ mod tests {
39
+ use super::*;
40
+
41
+ #[test]
42
+ fn reads_a_w3c_traceparent() {
43
+ let p = parent("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01").unwrap();
44
+ assert_eq!(p.trace_id().to_string(), "4bf92f3577b34da6a3ce929d0e0e4736");
45
+ assert_eq!(p.span_id().to_string(), "00f067aa0ba902b7");
46
+ assert!(p.is_sampled());
47
+ assert!(p.is_remote());
48
+ }
49
+
50
+ #[test]
51
+ fn refuses_what_isnt_one() {
52
+ for bad in [
53
+ "",
54
+ "junk",
55
+ "01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
56
+ "00-00000000000000000000000000000000-00f067aa0ba902b7-01",
57
+ "00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01",
58
+ ] {
59
+ assert!(parent(bad).is_none(), "{bad}");
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,203 @@
1
+ //! `weft::router()`: an axum `Router`, so a sidecar can be one binary or part of a Loco or axum app. Each call is
2
+ //! verified, deserialized, run in its own task with a deadline, and answered as
3
+ //!
4
+ //! ```text
5
+ //! { "ok": true, "value": …, "tags": […], "touched": […] }
6
+ //! { "ok": false, "error": { "code": "NotFound", "data": … } }
7
+ //! ```
8
+
9
+ use std::sync::Arc;
10
+ use std::time::{Duration, SystemTime, UNIX_EPOCH};
11
+
12
+ use axum::Router;
13
+ use axum::body::Bytes;
14
+ use axum::extract::{Path, State};
15
+ use axum::http::{HeaderMap, StatusCode, header};
16
+ use axum::response::{IntoResponse, Response};
17
+ use axum::routing::{get, post};
18
+ use serde_json::{Value, json};
19
+ use tracing::Instrument;
20
+
21
+ use crate::ctx::Base;
22
+ use crate::envelope::{Envelope, verify};
23
+ use crate::functions::{Answer, Failed, Functions, drift_body, internal_body, ok_body};
24
+
25
+ struct Shared {
26
+ functions: Functions,
27
+ secret: Vec<u8>,
28
+ }
29
+
30
+ /// The `Router`: `POST /_weft/fn/:id`, and `GET /_weft/health` for the supervisor. The key comes from `WEFT_RUST_SECRET`.
31
+ ///
32
+ /// # Panics
33
+ /// With no `WEFT_RUST_SECRET`: a sidecar that took unsigned calls would trust any process that could reach it.
34
+ pub fn router(functions: Functions) -> Router {
35
+ let secret = std::env::var("WEFT_RUST_SECRET").unwrap_or_default();
36
+ assert!(
37
+ !secret.is_empty(),
38
+ "R106: WEFT_RUST_SECRET is not set. `weft dev` and `weft start` set it; a sidecar deployed on its own needs the same value as the server"
39
+ );
40
+ router_with(functions, secret.into_bytes())
41
+ }
42
+
43
+ /// [`router`] with the key in hand.
44
+ pub fn router_with(functions: Functions, secret: Vec<u8>) -> Router {
45
+ Router::new()
46
+ .route("/_weft/fn/{id}", post(call))
47
+ .route("/_weft/health", get(health))
48
+ .with_state(Arc::new(Shared { functions, secret }))
49
+ }
50
+
51
+ async fn health(State(s): State<Arc<Shared>>) -> Response {
52
+ json_response(
53
+ StatusCode::OK,
54
+ &json!({ "ok": true, "service": s.functions.service(), "contract": s.functions.hash(), "functions": s.functions.ids(), "pid": std::process::id() }),
55
+ )
56
+ }
57
+
58
+ fn json_response(status: StatusCode, body: &Value) -> Response {
59
+ text_response(status, body.to_string())
60
+ }
61
+
62
+ fn text_response(status: StatusCode, body: String) -> Response {
63
+ (status, [(header::CONTENT_TYPE, "application/json")], body).into_response()
64
+ }
65
+
66
+ fn refused(status: StatusCode, code: &str, message: impl std::fmt::Display) -> Response {
67
+ json_response(
68
+ status,
69
+ &json!({ "ok": false, "error": { "code": code, "message": message.to_string() } }),
70
+ )
71
+ }
72
+
73
+ fn now_ms() -> u64 {
74
+ SystemTime::now()
75
+ .duration_since(UNIX_EPOCH)
76
+ .map_or(0, |d| d.as_millis() as u64)
77
+ }
78
+
79
+ /// `00-<trace>-<span>-<flags>` split, or nothing if it isn't one.
80
+ fn trace_ids(traceparent: Option<&str>) -> (&str, &str) {
81
+ let mut parts = traceparent.unwrap_or("").split('-');
82
+ match (parts.next(), parts.next(), parts.next()) {
83
+ (Some("00"), Some(trace), Some(span)) if trace.len() == 32 && span.len() == 16 => {
84
+ (trace, span)
85
+ }
86
+ _ => ("", ""),
87
+ }
88
+ }
89
+
90
+ /// Dropping this stops the task: axum drops the handler's future when TS hangs up.
91
+ struct AbortOnDrop(tokio::task::AbortHandle);
92
+ impl Drop for AbortOnDrop {
93
+ fn drop(&mut self) {
94
+ self.0.abort();
95
+ }
96
+ }
97
+
98
+ async fn call(
99
+ State(s): State<Arc<Shared>>,
100
+ Path(id): Path<String>,
101
+ headers: HeaderMap,
102
+ body: Bytes,
103
+ ) -> Response {
104
+ let text = |name: &str| headers.get(name).and_then(|v| v.to_str().ok());
105
+ let (Some(ctx), Some(sig)) = (text("weft-ctx"), text("weft-sig")) else {
106
+ return refused(
107
+ StatusCode::UNAUTHORIZED,
108
+ "Refused",
109
+ crate::envelope::Refused::Malformed,
110
+ );
111
+ };
112
+ let envelope: Envelope = match verify(&s.secret, &id, ctx, sig, &body, now_ms()) {
113
+ Ok(e) => e,
114
+ Err(e) => return refused(StatusCode::UNAUTHORIZED, "Refused", e),
115
+ };
116
+ let Some(handler) = s.functions.handler(&id) else {
117
+ return refused(
118
+ StatusCode::NOT_FOUND,
119
+ "Drift",
120
+ format!("this sidecar has no function `{id}`: rebuild it from the current contract"),
121
+ );
122
+ };
123
+ let budget = Duration::from_millis(envelope.deadline_ms);
124
+ let base = Base::new(&id, envelope);
125
+ let (trace_id, parent_id) = trace_ids(base.traceparent());
126
+ let span = tracing::info_span!(
127
+ "weft.fn",
128
+ otel.name = id.as_str(),
129
+ function = id.as_str(),
130
+ request_id = base.request_id(),
131
+ trace_id,
132
+ parent_id
133
+ );
134
+ #[cfg(feature = "otel")]
135
+ crate::otel::link(&span, base.traceparent());
136
+
137
+ // A task of its own, so a panic is one failed call and not a dropped connection.
138
+ let task = tokio::spawn(handler(base.clone(), Vec::from(body)).instrument(span.clone()));
139
+ let _abort = AbortOnDrop(task.abort_handle());
140
+ let outcome = tokio::time::timeout(budget, task).await;
141
+ let _enter = span.enter();
142
+ let answer = match outcome {
143
+ Ok(Ok(answer)) => answer,
144
+ Ok(Err(e)) if e.is_panic() => Answer::Failed(Failed::Internal(format!(
145
+ "panicked: {}",
146
+ crate::functions::panic_text(&*e.into_panic())
147
+ ))),
148
+ Ok(Err(e)) => Answer::Failed(Failed::Internal(e.to_string())),
149
+ Err(_) => {
150
+ tracing::warn!(budget_ms = budget.as_millis() as u64, "deadline passed");
151
+ return refused(
152
+ StatusCode::GATEWAY_TIMEOUT,
153
+ "Deadline",
154
+ format!("`{id}` ran past its deadline"),
155
+ );
156
+ }
157
+ };
158
+ match answer {
159
+ Answer::Value(value) => text_response(StatusCode::OK, ok_body(&value, &base)),
160
+ Answer::Failed(Failed::Typed(error)) => {
161
+ json_response(StatusCode::OK, &json!({ "ok": false, "error": error }))
162
+ }
163
+ Answer::Failed(Failed::Internal(detail)) => {
164
+ tracing::error!(detail, "failed");
165
+ json_response(
166
+ StatusCode::INTERNAL_SERVER_ERROR,
167
+ &internal_body(&base, None),
168
+ )
169
+ }
170
+ Answer::Drift(detail) => {
171
+ tracing::error!(detail, "drift");
172
+ json_response(StatusCode::UNPROCESSABLE_ENTITY, &drift_body(&detail))
173
+ }
174
+ }
175
+ }
176
+
177
+ /// Serve on `WEFT_LISTEN` (`127.0.0.1:0` when unset), print `weft-listening <addr>` for the supervisor, and drain on
178
+ /// SIGTERM or Ctrl-C. Logs go to stderr, by `RUST_LOG` (`info`), unless the app installed a subscriber first.
179
+ pub async fn serve(router: Router) -> std::io::Result<()> {
180
+ use tracing_subscriber::EnvFilter;
181
+ let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
182
+ let _ = tracing_subscriber::fmt()
183
+ .with_env_filter(filter)
184
+ .with_writer(std::io::stderr)
185
+ .try_init();
186
+ let addr = std::env::var("WEFT_LISTEN").unwrap_or_else(|_| "127.0.0.1:0".into());
187
+ let listener = tokio::net::TcpListener::bind(&addr).await?;
188
+ println!("weft-listening {}", listener.local_addr()?);
189
+ axum::serve(listener, router)
190
+ .with_graceful_shutdown(stopped())
191
+ .await
192
+ }
193
+
194
+ async fn stopped() {
195
+ #[cfg(unix)]
196
+ {
197
+ use tokio::signal::unix::{SignalKind, signal};
198
+ let mut term = signal(SignalKind::terminate()).expect("a SIGTERM handler");
199
+ tokio::select! { _ = term.recv() => {}, _ = tokio::signal::ctrl_c() => {} }
200
+ }
201
+ #[cfg(not(unix))]
202
+ let _ = tokio::signal::ctrl_c().await;
203
+ }
package/package.json CHANGED
@@ -1,28 +1,31 @@
1
1
  {
2
2
  "name": "@weft-js/start",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "weft Start: file routes, server functions, streaming SSR, a database and auth",
5
+ "keywords": [
6
+ "weft"
7
+ ],
5
8
  "license": "MIT",
6
- "type": "module",
7
- "dependencies": {
8
- "@fontsource/inter": "5.3.0",
9
- "@takumi-rs/core": "2.18.2",
10
- "@takumi-rs/helpers": "2.18.2",
11
- "@takumi-rs/wasm": "2.18.2",
12
- "@tanstack/history": "1.162.5",
13
- "@tanstack/router-core": "1.171.39",
14
- "nitro": "3.0.260903-beta",
15
- "vite": "8.3.5"
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/Ravonus/weft.git",
12
+ "directory": "packages/start"
16
13
  },
17
- "peerDependencies": {
18
- "better-auth": "^1.7.0",
19
- "drizzle-orm": "^0.45.0",
20
- "drizzle-zod": "^0.8.0",
21
- "mysql2": "^3.24.0",
22
- "postgres": ">=3",
23
- "@weft-js/core": "^0.1.1",
24
- "zod": "^4.0.0"
14
+ "homepage": "https://github.com/Ravonus/weft/tree/main/packages/start#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/Ravonus/weft/issues"
25
17
  },
18
+ "files": [
19
+ "src",
20
+ "crate",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "type": "module",
25
+ "sideEffects": [
26
+ "**/*.css",
27
+ "./src/share.ts"
28
+ ],
26
29
  "exports": {
27
30
  ".": "./src/index.ts",
28
31
  "./data": "./src/data.ts",
@@ -61,10 +64,28 @@
61
64
  "./rust": "./src/rust.ts",
62
65
  "./sidecar": "./src/sidecar.mjs"
63
66
  },
64
- "sideEffects": [
65
- "**/*.css",
66
- "./src/share.ts"
67
- ],
67
+ "publishConfig": {
68
+ "access": "public"
69
+ },
70
+ "dependencies": {
71
+ "@fontsource/inter": "5.3.0",
72
+ "@takumi-rs/core": "2.18.2",
73
+ "@takumi-rs/helpers": "2.18.2",
74
+ "@takumi-rs/wasm": "2.18.2",
75
+ "@tanstack/history": "1.162.5",
76
+ "@tanstack/router-core": "1.171.39",
77
+ "nitro": "3.0.260903-beta",
78
+ "vite": "8.3.5"
79
+ },
80
+ "peerDependencies": {
81
+ "@weft-js/core": "^0.1.3",
82
+ "better-auth": "^1.7.0",
83
+ "drizzle-orm": "^0.45.0",
84
+ "drizzle-zod": "^0.8.0",
85
+ "mysql2": "^3.24.0",
86
+ "postgres": ">=3",
87
+ "zod": "^4.0.0"
88
+ },
68
89
  "peerDependenciesMeta": {
69
90
  "better-auth": {
70
91
  "optional": true
@@ -91,19 +112,7 @@
91
112
  "optional": true
92
113
  }
93
114
  },
94
- "publishConfig": {
95
- "access": "public"
96
- },
97
115
  "engines": {
98
116
  "node": ">=26"
99
- },
100
- "keywords": [
101
- "weft"
102
- ],
103
- "files": [
104
- "src",
105
- "crate",
106
- "README.md",
107
- "LICENSE"
108
- ]
109
- }
117
+ }
118
+ }
package/src/app.mjs CHANGED
@@ -15,21 +15,39 @@ import weftStart, { startDir } from './vite.mjs'
15
15
  function usesSockets(root) {
16
16
  const src = join(root, 'src')
17
17
  if (!existsSync(src)) return false
18
- return readdirSync(src, { recursive: true, withFileTypes: true }).some(f => f.isFile() && /\.[cm]?[jt]sx?$/.test(f.name) && /\b(channel|sharedState)\s*[(<]/.test(readFileSync(join(f.parentPath, f.name), 'utf8')))
18
+ return readdirSync(src, { recursive: true, withFileTypes: true }).some(
19
+ (f) =>
20
+ f.isFile() &&
21
+ /\.[cm]?[jt]sx?$/.test(f.name) &&
22
+ /\b(channel|sharedState)\s*[(<]/.test(readFileSync(join(f.parentPath, f.name), 'utf8')),
23
+ )
19
24
  }
20
25
 
21
- function config({ root, site, hydration, preset, port, logLevel, command, nitro: nitroOptions = {} }) {
26
+ function config({
27
+ root,
28
+ site,
29
+ hydration,
30
+ preset,
31
+ port,
32
+ logLevel,
33
+ command,
34
+ nitro: nitroOptions = {},
35
+ }) {
22
36
  root = resolve(root)
23
37
  const sockets = usesSockets(root)
24
38
  const runner = devRunner(sockets)
25
- const user = ['vite.config.ts', 'vite.config.mts', 'vite.config.js', 'vite.config.mjs'].map(f => join(root, f)).find(existsSync)
39
+ const user = ['vite.config.ts', 'vite.config.mts', 'vite.config.js', 'vite.config.mjs']
40
+ .map((f) => join(root, f))
41
+ .find(existsSync)
26
42
  return {
27
43
  root,
28
44
  configFile: user ?? false,
29
45
  logLevel,
30
46
  server: { port: port ?? 5173, strictPort: port !== undefined },
31
47
  // A function host can't hold a socket: the page goes straight to SSE, and a channel says `unavailable` (R024)
32
- ...(command === 'build' && { define: { __WEFT_SOCKETS__: JSON.stringify(holdsSockets(preset)) } }),
48
+ ...(command === 'build' && {
49
+ define: { __WEFT_SOCKETS__: JSON.stringify(holdsSockets(preset)) },
50
+ }),
33
51
  plugins: [
34
52
  weft({ hydratable: true }),
35
53
  weftStart({ root, site, hydration }),
@@ -38,7 +56,11 @@ function config({ root, site, hydration, preset, port, logLevel, command, nitro:
38
56
  preset,
39
57
  // Every app gets this: built assets ship as .br and .gz next to the file, and Nitro picks by Accept-Encoding.
40
58
  compressPublicAssets: { gzip: true, brotli: true },
41
- ...(site?.headers && { routeRules: Object.fromEntries(Object.entries(site.headers).map(([path, headers]) => [path, { headers }])) }),
59
+ ...(site?.headers && {
60
+ routeRules: Object.fromEntries(
61
+ Object.entries(site.headers).map(([path, headers]) => [path, { headers }]),
62
+ ),
63
+ }),
42
64
  ...(sockets && { features: { websocket: true } }),
43
65
  ...(runner && { devServer: { runner } }),
44
66
  // Build mode for share cards (research 20 §4.3): the server lists them and Nitro's prerender writes the PNGs.
@@ -46,9 +68,14 @@ function config({ root, site, hydration, preset, port, logLevel, command, nitro:
46
68
  // Prerendered cards are static files at content-addressed URLs: immutable. The rule names each file (a
47
69
  // `/_weft/og/**` rule would also override the handler's no-cache for a stale `?v=` on demand). Prerender runs
48
70
  // before Nitro bundles the server, so rules added when it is done are in the build. (Dev sends no-store.)
49
- ...(command === 'build' && { hooks: immutableCards(), plugins: [fileURLToPath(new URL('./range.mjs', import.meta.url))] }),
71
+ ...(command === 'build' && {
72
+ hooks: immutableCards(),
73
+ plugins: [fileURLToPath(new URL('./range.mjs', import.meta.url))],
74
+ }),
50
75
  // The media route: sharp's addon, and css-tree and csso (under svgo) read JSON from their packages at runtime.
51
- ...(existsSync(join(root, '.weft/slots/media.server.ts')) && { traceDeps: ['css-tree*', 'csso*', 'sharp*'] }),
76
+ ...(existsSync(join(root, '.weft/slots/media.server.ts')) && {
77
+ traceDeps: ['css-tree*', 'csso*', 'sharp*'],
78
+ }),
52
79
  ...nitroOptions,
53
80
  }),
54
81
  ],
@@ -69,7 +96,10 @@ function immutableCards() {
69
96
  'prerender:done'({ prerenderedRoutes }) {
70
97
  if (!built) return
71
98
  for (const r of prerenderedRoutes) {
72
- if (r.route.startsWith('/_weft/og/') && r.route.endsWith('.png')) built.options.routeRules[r.route] = { headers: { 'cache-control': 'public, max-age=31536000, immutable' } }
99
+ if (r.route.startsWith('/_weft/og/') && r.route.endsWith('.png'))
100
+ built.options.routeRules[r.route] = {
101
+ headers: { 'cache-control': 'public, max-age=31536000, immutable' },
102
+ }
73
103
  }
74
104
  built.routing.sync()
75
105
  },
@@ -0,0 +1,13 @@
1
+ import type { AssetReceipt, RouteAssetCosts } from './router'
2
+
3
+ /** Select only matched levels and union their emitted files for this build. */
4
+ export function selectRouteAssets(
5
+ receipt: AssetReceipt | null | undefined,
6
+ build: string | undefined,
7
+ ids: string[],
8
+ ): RouteAssetCosts | null
9
+ /** The costs, if they were measured for this build. */
10
+ export function matchingRouteAssets(
11
+ costs: RouteAssetCosts | undefined,
12
+ build: string | null | undefined,
13
+ ): RouteAssetCosts | null
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Select only matched levels and union their emitted files for this build.
3
+ * @param {import('./router').AssetReceipt | null | undefined} receipt
4
+ * @param {string | undefined} build
5
+ * @param {string[]} ids
6
+ * @returns {import('./router').RouteAssetCosts | null}
7
+ */
8
+ export function selectRouteAssets(receipt, build, ids) {
9
+ if (
10
+ !build ||
11
+ !receipt ||
12
+ receipt.build !== build ||
13
+ receipt.version !== 1 ||
14
+ receipt.scope !== 'vite-static-closure' ||
15
+ !receipt.routes ||
16
+ !receipt.files ||
17
+ !ids.length
18
+ )
19
+ return null
20
+ const levels = ids.map((id) => ({
21
+ id,
22
+ entry: Object.hasOwn(receipt.routes, id) ? receipt.routes[id] : null,
23
+ }))
24
+ const union = (entries) => {
25
+ const names = new Set()
26
+ for (const entry of entries) {
27
+ if (
28
+ !entry ||
29
+ !Array.isArray(entry.files) ||
30
+ typeof entry.bytes !== 'number' ||
31
+ !Number.isSafeInteger(entry.bytes) ||
32
+ entry.bytes < 0
33
+ )
34
+ return null
35
+ for (const file of entry.files) names.add(file)
36
+ }
37
+ let total = 0
38
+ for (const file of names) {
39
+ const bytes = Object.hasOwn(receipt.files, file) ? receipt.files[file]?.bytes : null
40
+ if (typeof bytes !== 'number' || !Number.isSafeInteger(bytes) || bytes < 0) return null
41
+ total += bytes
42
+ if (!Number.isSafeInteger(total)) return null
43
+ }
44
+ return total
45
+ }
46
+ return {
47
+ build,
48
+ scope: receipt.scope,
49
+ levels: levels.map(({ id, entry }) => ({ id, bytes: union([entry]) })),
50
+ routeBytes: union(levels.map(({ entry }) => entry)),
51
+ eagerBytes: union([receipt.eager]),
52
+ combinedBytes: union([...levels.map(({ entry }) => entry), receipt.eager]),
53
+ }
54
+ }
55
+
56
+ /** @param {import('./router').RouteAssetCosts | undefined} costs
57
+ * @param {string | null | undefined} build
58
+ * @returns {import('./router').RouteAssetCosts | null} */
59
+ export function matchingRouteAssets(costs, build) {
60
+ return costs?.build && costs.build === build ? costs : null
61
+ }