@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.
- package/README.md +1278 -12
- package/crate/Cargo.lock +1347 -0
- package/crate/Cargo.toml +72 -0
- package/crate/README.md +322 -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,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.
|
|
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
|
-
"
|
|
7
|
-
|
|
8
|
-
"
|
|
9
|
-
"
|
|
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
|
-
"
|
|
18
|
-
|
|
19
|
-
"
|
|
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
|
-
"
|
|
65
|
-
"
|
|
66
|
-
|
|
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
|
-
|
|
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(
|
|
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({
|
|
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']
|
|
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' && {
|
|
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 && {
|
|
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' && {
|
|
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')) && {
|
|
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'))
|
|
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
|
+
}
|