lecodes-sdk 2.0.4 → 2.0.5

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 (72) hide show
  1. package/README.md +104 -76
  2. package/dist/global.d.ts +2 -5
  3. package/dist/host.d.ts +3 -0
  4. package/dist/types/inject.d.ts +3 -3
  5. package/dist/types/runtime/device.d.ts +7 -0
  6. package/dist/types/runtime/rpc.d.ts +11 -17
  7. package/dist/types/runtime/wire.d.ts +53 -0
  8. package/dist/types/server/auth/api.d.ts +42 -0
  9. package/dist/types/server/auth/appConfig.d.ts +1 -5
  10. package/dist/types/server/auth/models.d.ts +119 -70
  11. package/dist/types/server/auth/types.d.ts +19 -43
  12. package/dist/types/server/channel.d.ts +57 -19
  13. package/dist/types/server/context.d.ts +2 -2
  14. package/dist/types/server/db/defineDb.d.ts +10 -0
  15. package/dist/types/server/db/index.d.ts +1 -1
  16. package/dist/types/server/db/types.d.ts +76 -6
  17. package/dist/types/server/inject.d.ts +0 -1
  18. package/dist/types/ui/UINode.d.ts +19 -5
  19. package/dist/types/ui/UIScreen.d.ts +1 -0
  20. package/dist/types/ui/UITabs.d.ts +8 -6
  21. package/dist/types/ui/theme.d.ts +48 -13
  22. package/dist/types/version.d.ts +1 -1
  23. package/dist/types.json +1 -1
  24. package/package.json +3 -2
  25. package/prompts/README.md +1 -1
  26. package/prompts/design.md +19 -19
  27. package/prompts/dist/2d-game.md +45 -31
  28. package/prompts/dist/3d-app.md +45 -31
  29. package/prompts/dist/ar-app.md +45 -31
  30. package/prompts/dist/design.md +25 -24
  31. package/prompts/dist/ui-app.md +45 -31
  32. package/prompts/ui-design.md +6 -5
  33. package/prompts/ui.md +25 -22
  34. package/src/bridges/device.d.ts +9 -0
  35. package/src/bridges/tree.d.ts +5 -0
  36. package/src/chisel.ts +1 -1
  37. package/src/compile/bundler.ts +6 -0
  38. package/src/compile/compileProject.ts +3 -1
  39. package/src/compile/index.ts +3 -1
  40. package/src/compile/serverSplit.ts +58 -11
  41. package/src/compile/serverTypes.ts +189 -8
  42. package/src/host.d.ts +3 -0
  43. package/src/inject.ts +6 -6
  44. package/src/runtime/device.ts +12 -0
  45. package/src/runtime/rpc.ts +101 -40
  46. package/src/runtime/wire.ts +35 -0
  47. package/src/server/auth/api.ts +94 -0
  48. package/src/server/auth/appConfig.ts +2 -3
  49. package/src/server/auth/host.ts +244 -174
  50. package/src/server/auth/models.ts +45 -62
  51. package/src/server/auth/types.ts +19 -34
  52. package/src/server/channel.ts +97 -29
  53. package/src/server/channelHub.ts +153 -0
  54. package/src/server/context.ts +2 -2
  55. package/src/server/db/defineDb.ts +96 -36
  56. package/src/server/db/index.ts +1 -1
  57. package/src/server/db/types.ts +76 -8
  58. package/src/server/host.ts +25 -10
  59. package/src/server/inject.ts +2 -2
  60. package/src/server/runtime.ts +34 -12
  61. package/src/ui/UINode.ts +22 -5
  62. package/src/ui/UIScreen.ts +5 -0
  63. package/src/ui/UITabs.ts +19 -17
  64. package/src/ui/styleColor.ts +10 -1
  65. package/src/ui/theme.ts +96 -41
  66. package/src/version.ts +1 -1
  67. package/tests/helpers/fakeTree.ts +1 -0
  68. package/dist/types/plugins/oauth.d.ts +0 -25
  69. package/dist/types/server/auth/global.d.ts +0 -56
  70. package/src/plugins/oauth.ts +0 -61
  71. package/src/server/auth/global.ts +0 -80
  72. package/tests/helpers/memoryMarci.ts +0 -124
@@ -191,7 +191,7 @@ const label: Style<UIText> = { fontSize: 12, fontWeight: 600, letterSpacing: 2 }
191
191
  // Padding: p, px, py, pt, pb, pl, pr (≡ padding, paddingHorizontal, paddingVertical, paddingTop, ...)
192
192
  // Margin: m, mx, my, mt, mb, ml, mr — margins also accept "auto" (mx: "auto" centers a fixed-width element)
193
193
  // Background (containers/screen/button; UIText/UIImage/UISpacer have bgColor only):
194
- // bgColor, bgImage (string | imported asset), bgSize ("cover"|"contain"|"tile"), bgGradient
194
+ // bgColor, bgImage (string | imported asset), bgSize ("cover"|"contain"|"fill"|"tile"), bgGradient
195
195
  // bgGradient: a CSS linear-gradient() or radial-gradient() string, e.g. "linear-gradient(to top, rgba(0,0,0,0.7), transparent)".
196
196
  // Layers paint bottom-to-top: bgColor → bgImage → bgGradient (gradient over image = text scrim).
197
197
  // bgImage is decoration BEHIND children — an image that IS the content belongs in UIImage.
@@ -221,10 +221,11 @@ const label: Style<UIText> = { fontSize: 12, fontWeight: 600, letterSpacing: 2 }
221
221
 
222
222
  // --- Theme variables ---
223
223
  // theme({...}) merges app vars into one table; styles read them with "var(--name)" (numbers are px):
224
- const T = theme({ primaryColor: "#0A84FF" })
225
- label.style({ color: T.primaryColor }) // or "var(--primaryColor)"
226
- // theme({ color, fontFamily }) drive the DEFAULT text color/font app-wide. "var(--name, fallback)"
227
- // applies the fallback while the key is unset — how the SDK tab bar (defineTabs) stays themeable.
224
+ const T = theme({ accent: "#0A84FF" })
225
+ label.style({ color: T.accent }) // or "var(--accent)"
226
+ // ROLES the SDK reads itself: bg (screens) · surface / border (the tab bar) · text / fontFamily
227
+ // (text that names none) · textMuted (inactive tabs) · accent (the active tab). "var(--name,
228
+ // fallback)" applies the fallback while the key is unset.
228
229
 
229
230
  // --- Defaults that surprise ---
230
231
  // flexShrink: 0 — elements don't shrink to fit (exception: UIScrollable defaults flexShrink: 1 so it scrolls
@@ -327,20 +328,20 @@ composition:
327
328
 
328
329
  ```ts
329
330
  // design/shared/tokens.ts — surfaces dark-to-light, text in tiers, accents as a family.
330
- // ONE theme() call registers the vars and returns the accessors ("var(--x)" strings); the system
331
- // key `color` in the same table sets the app-wide default text color. Re-calling theme() with new
332
- // values (dark mode, brand swap) restyles every screen — and later the built app — live.
333
- const palette = {
334
- bg: "#080808", card: "#161618", cardAlt: "#1d1d20", field: "#101012",
335
- border: "#2a2a2e", text: "#ffffff", muted: "#8a8a93", faint: "#5a5b62",
336
- accent: "#00BFC2", accentPressed: "#00999b", accentDeep: "#0c4e4f", onAccent: "#05201f",
331
+ // ONE theme() call registers the vars and returns the accessors ("var(--x)" strings). Re-calling
332
+ // theme() with new values (dark mode, brand swap) restyles every screen — and later the built app
333
+ // — live.
334
+ export const colors = theme({
335
+ // the ROLES — the SDK reads these too: bg = every screen's background, text = text that names
336
+ // no color, surface / border / textMuted / accent = the tab bar (defineTabs here, UITabs in the
337
+ // built app). Name the palette by them; there are no separate keys for the chrome.
338
+ bg: "#080808", surface: "#161618", border: "#2a2a2e",
339
+ text: "#ffffff", textMuted: "#8a8a93", accent: "#00BFC2",
340
+ // the design's own
341
+ surfaceAlt: "#1d1d20", field: "#101012", faint: "#5a5b62",
342
+ accentPressed: "#00999b", accentDeep: "#0c4e4f", onAccent: "#05201f",
337
343
  success: "#30D158", danger: "#F83131", scrim: "#000000b3",
338
- // theme SYSTEM keys — SDK-rendered chrome reads these. `color` = default text color everywhere;
339
- // the other four brand the tab bar (defineTabs here, UITabs in the built app). Mirror the brand:
340
- color: "#ffffff",
341
- primaryColor: "#00BFC2", mutedColor: "#8a8a93", tabbarBg: "#161618", screenBg: "#080808",
342
- }
343
- export const colors: { [K in keyof typeof palette]: string } = theme(palette)
344
+ })
344
345
  export const spacing = { xs: 4, sm: 8, md: 12, lg: 16, xl: 20, xxl: 24 }
345
346
  export const radius = { sm: 8, md: 12, lg: 16, pill: 999 }
346
347
  ```
@@ -350,7 +351,7 @@ export const radius = { sm: 8, md: 12, lg: 16, pill: 999 }
350
351
  import { colors, spacing, radius } from "./tokens"
351
352
 
352
353
  export const PrimaryButton = (label: string, o: { name?: string, disabled?: boolean } = {}) =>
353
- UIButton(UIText(label).style({ fontSize: 16, fontWeight: 700, color: o.disabled ? colors.muted : colors.onAccent }))
354
+ UIButton(UIText(label).style({ fontSize: 16, fontWeight: 700, color: o.disabled ? colors.textMuted : colors.onAccent }))
354
355
  .style({ height: 52, borderRadius: radius.md, bgColor: o.disabled ? colors.accentDeep : colors.accent,
355
356
  justifyContent: "center", alignItems: "center", name: o.name, $pressed: { bgColor: colors.accentPressed } })
356
357
 
@@ -361,7 +362,7 @@ export const SectionLabel = (t: string) =>
361
362
  export const Field = (o: { label?: string, value?: string, placeholder?: string, focused?: boolean }) =>
362
363
  UIColumn(
363
364
  o.label ? SectionLabel(o.label) : null,
364
- UIRow(UIText(o.value ?? o.placeholder ?? "").style({ flexGrow: 1, fontSize: 16, color: o.value ? colors.text : colors.muted }))
365
+ UIRow(UIText(o.value ?? o.placeholder ?? "").style({ flexGrow: 1, fontSize: 16, color: o.value ? colors.text : colors.textMuted }))
365
366
  .style({ height: 52, px: 16, alignItems: "center", bgColor: colors.field, borderRadius: radius.md,
366
367
  border: `1px solid ${o.focused ? colors.accent : colors.border}` }),
367
368
  ).style({ gap: spacing.sm })
@@ -380,9 +381,9 @@ export const ConfirmDialog = (o: { title: string, body?: string, confirmLabel: s
380
381
  UIWidget(
381
382
  UIColumn(
382
383
  UIText(o.title).style({ fontSize: 17, fontWeight: 700, color: colors.text, textAlign: "center" }),
383
- o.body ? UIText(o.body).style({ fontSize: 14, color: colors.muted, textAlign: "center", lineHeight: "1.4em" }) : null,
384
+ o.body ? UIText(o.body).style({ fontSize: 14, color: colors.textMuted, textAlign: "center", lineHeight: "1.4em" }) : null,
384
385
  PrimaryButton("Cancel", { name: "cancel" }),
385
- ).style({ bgColor: colors.card, borderRadius: radius.lg, p: spacing.xl, gap: spacing.lg, width: "86%", maxWidth: 360 }),
386
+ ).style({ bgColor: colors.surface, borderRadius: radius.lg, p: spacing.xl, gap: spacing.lg, width: "86%", maxWidth: 360 }),
386
387
  ).style({ top: 0, left: 0, right: 0, bottom: 0, justifyContent: "center", alignItems: "center", overlayColor: colors.scrim })
387
388
  ```
388
389
 
@@ -479,8 +480,8 @@ export const mainTabs = defineTabs({
479
480
  A screen that shows the bar mounts it with a literal tab id — roots AND pushed details that keep
480
481
  the bar; a screen that doesn't mount it is a full-screen push. Mount the bar alone when the screen
481
482
  owns its layout (`mainTabs.bar("schedule")`, as above) or wrap content with
482
- `mainTabs.screen("home", [...])`. Restyle through the theme's system keys — `primaryColor`,
483
- `mutedColor`, `tabbarBg`, `screenBg`, set in `shared/tokens.ts` — never by reimplementing the
483
+ `mainTabs.screen("home", [...])`. It is colored by the theme's roles — `accent`,
484
+ `textMuted`, `surface`, `border`, `bg`, set in `shared/tokens.ts` — never by reimplementing the
484
485
  bar. Keep the `defineTabs({...})` declaration and `.screen("<tab>", ...)` / `.bar("<tab>")` call
485
486
  shapes intact — the board parses them.
486
487
 
@@ -403,8 +403,8 @@ Router.init(tabs) // UITabs IS a UIScreen — present it directly
403
403
  // .select(id), .tab (getter), .onSelect(cb(id, i)) — fires on a bar tap, swipe, or select()
404
404
  // .badge(id, value) — true = dot, number/string = count pill, false/null/0 clears
405
405
  // .pager — the UIPager underneath; UIPager.push(detail) from any screen keeps the bar
406
- // Styling is THEME-driven: theme({ primaryColor, mutedColor, tabbarBg, tabbarBorder, badgeColor })
407
- // restyles the bar app-wide (dark fallbacks built in). For a custom bar layout use UIPager below.
406
+ // The bar is colored by the theme's roles (accent, textMuted, surface, border) — no keys of its
407
+ // own; one bar's own colors: tabs.bar.theme({ accent, surface }). Custom layout: UIPager below.
408
408
 
409
409
  // UIPager — the navigation primitive under UITabs: sibling tabs that swipe natively, each tab its
410
410
  // OWN push/pop stack. Reach for it directly for a plain stack (one-screen pager) or a fully custom
@@ -620,7 +620,7 @@ const btn: Style<UIButton> = { bgColor: "#333", borderRadius: 12 }
620
620
  // Padding: p, px, py, pt, pb, pl, pr (≡ padding, paddingHorizontal, paddingVertical, paddingTop, ...)
621
621
  // Margin: m, mx, my, mt, mb, ml, mr — margins also accept "auto" (mx: "auto" centers a fixed-width element)
622
622
  // Background (containers/screen/button/input/video; UIText/UIImage/UISpacer have bgColor only):
623
- // bgColor, bgImage (string | FetchResponse | File), bgSize ("cover"|"contain"|"tile"), bgGradient
623
+ // bgColor, bgImage (string | FetchResponse | File | SvgSource), bgSize ("cover"|"contain"|"fill"|"tile"), bgGradient
624
624
  // bgGradient: a CSS linear-gradient() or radial-gradient() string, e.g. "linear-gradient(to top, rgba(0,0,0,0.7), transparent)"
625
625
  // or "radial-gradient(circle at 50% 40%, #7B2FF7, #0a0a1a)". Comma-separate several gradients to stack them (first = on top).
626
626
  // Layers paint bottom-to-top: bgColor → bgImage → bgGradient (gradient over image = text scrim).
@@ -647,23 +647,27 @@ const btn: Style<UIButton> = { bgColor: "#333", borderRadius: 12 }
647
647
  // Env keywords resolving to the device insets (notch, home indicator):
648
648
  // "safe-top" | "safe-bottom" | "safe-left" | "safe-right"; "safe-all" — p / m shorthands only
649
649
  // Comfort tokens — where content comfortably starts, never collapse to 0: max(safe-edge, knob) on clearance
650
- // insets (iOS notch/home indicator, gesture nav); past exact-height system BARS (Android status/nav bar) the
651
- // knob ADDS instead. Knob defaults: 12 top, 4 bottom, 16 horizontal (the page gutter):
650
+ // insets (iOS notch/home indicator); past exact-height system BARS (Android status/nav bar) the
651
+ // knob ADDS instead. Knob defaults: 12 top, 8 bottom, 16 horizontal (the page gutter):
652
652
  // "comfort-top" | "comfort-bottom" | "comfort-left" | "comfort-right"
653
653
  // "comfort-x" — px/mx | "comfort-y" — py/my | "comfort-all" — p / m
654
654
  // Accepted on per-side padding & margin, position offsets (top/left/bottom/right), and inside calc()/min()/max():
655
- .style({ pb: "comfort-bottom" }) // bottom bar: 34 over a home indicator, 52 above Android's button bar, 4 on inset-less devices
655
+ .style({ pb: "comfort-bottom" }) // bottom bar: 34 over a home indicator, 56 above Android's button bar, 8 on inset-less devices
656
656
  .style({ px: "comfort-x" }) // page gutter that also clears the notch in landscape
657
657
  .style({ pb: "calc(comfort-bottom + 56px)" }) // scroll content clearing a 56px floating bar
658
658
 
659
659
  // --- Theme variables ---
660
660
  // theme({...}) merges app vars into one table; styles read them with "var(--name)" (numbers are px
661
661
  // lengths, null removes a key). Re-calling re-styles the LIVE UI — dark mode is a second call.
662
- // Returned accessors ARE the var() strings; system vars are statics (theme.primaryColor, …):
663
- const T = theme({ primaryColor: "#0A84FF", "comfort-left": 20 })
664
- label.style({ color: T.primaryColor, pl: "var(--comfort-left)" })
665
- // SYSTEM DEFAULTS: theme({ color, fontFamily }) drive the DEFAULT text color/font — all text
666
- // without explicit values follows (light theme = set once, not color:"black" per label).
662
+ // ONE call per app. Returned accessors ARE the var() strings:
663
+ const T = theme({ accent: "#0A84FF", "comfort-left": 20 })
664
+ label.style({ color: T.accent, pl: "var(--comfort-left)" })
665
+ // ROLES — keys the SDK reads itself; use them as your palette and add your own keys beside them:
666
+ // bg (every screen's background) · surface (cards, bars; the tab bar) · border (hairlines)
667
+ // text (text with no color of its own) · textMuted (secondary; inactive tabs) · accent (active tab)
668
+ // fontFamily (text with no font of its own). Light theme = set text once, not a color per label.
669
+ // SCOPE: el.theme({...}) — the same keys for ONE subtree (a card, tabs.bar, a whole screen); its
670
+ // descendants read them first. A pushed screen / a widget is its own root: give it its own.
667
671
  // Bare comfort-* tokens apply the safe-area formula; "var(--comfort-top)" reads the raw knob.
668
672
  // "var(--name, fallback)" applies the fallback while the key is unset. Env names (safe-*, vw/vh) are not theme keys.
669
673
 
@@ -695,7 +699,7 @@ screen.style({ flexDirection: "column", p: 16, $landscape: { flexDirection: "row
695
699
  // alignItems: "stretch" — children FILL the cross axis by default; set a size or alignSelf to opt out.
696
700
  // overflow: "hidden" — children are clipped to the parent's box; set overflow: "visible" to let them escape.
697
701
  // boxSizing: "border-box" — width/height include padding & border.
698
- // Screen bg is black, default text is WHITE at fontSize 14 (retarget once via theme({ color, fontFamily })).
702
+ // Screen bg is black, default text is WHITE at fontSize 14 (retarget once via theme({ bg, text, fontFamily })).
699
703
  // On any light surface — white cards, sheets, inputs — white text renders invisible: use dark text there
700
704
  // ("#111"/black), checking every UIText against the surface it actually sits on, not the screen.
701
705
 
@@ -769,14 +773,25 @@ UIButton().onLayout(({ width }) => { buttonWidth = width })
769
773
  // ===== ROUTER (multi-page apps) =====
770
774
  // Tabs are UITabs' job, in-tab stacks UIPager's (see UI COMPONENTS) — the Router owns what sits ABOVE the shell.
771
775
  Router.init(homeScreen, opts?: { showDefaultBackButton?: boolean }) // call once in the entry file (default false)
772
- Router.push(screen) // push onto the stack, screen becomes active
773
- Router.pop(to?: number) // default -1 = one back; negative = relative (-2 = back two), 0/positive = absolute
776
+ Router.push(screen, opts?: { transition, popTransition }) // push onto the stack, screen becomes active (default "push")
777
+ Router.pop(to?: number, opts?: { transition })
778
+ // default -1 = one back; negative = relative (-2 = back two), 0/positive = absolute
774
779
  // stack index (0 = home). Popped screens' native trees are destroyed; the UIScreen
775
- // object stays reusable (push remounts it).
776
- Router.replace(screen, opts?: { transition }) // swap the top screen; transition: "slide-from-left" |
777
- // "slide-from-right" | "slide-from-top" | "slide-from-bottom" | "zoom" | "zoom-out" | "zoom-in" | "fade" | "none" (default "fade")
780
+ // object stays reusable (push remounts it). Plays the way back the screen remembers.
781
+ Router.replace(screen, opts?: { transition, popTransition }) // swap the top screen (default "none"; theme({ replaceTransition }))
778
782
  Router.current // the active UIScreen (getter)
779
- Router.hide() / Router.restore() // hide/restore the whole router — stack stays alive; fires the top screen's onClose/onOpen
783
+ Router.hide(opts?) / Router.restore(opts?) // hide/restore the whole router — stack stays alive; fires the top screen's onClose/onOpen
784
+ // TRANSITIONS — every call above, and open() / close() of any destination, takes { transition }:
785
+ // a name: "push" | "pop" | "slide-from-left" | "slide-from-right" | "slide-from-top" | "slide-from-bottom" |
786
+ // "zoom" | "zoom-in" | "zoom-out" | "fade" | "none"
787
+ // or your own: { enter?: pose, exit?: pose, onTop?: "enter" | "exit" } — the incoming screen comes FROM `enter`,
788
+ // the outgoing one goes TO `exit`. pose = { transform?, opacity?, dim?, duration? (ms, 300), delay?, easing? };
789
+ // arrays are keyframes; in a pose a % in translate is a percent of the screen ("translateY(100%)").
790
+ // e.g. Router.push(page, { transition: { enter: { transform: "translateY(100%)", duration: 400 }, exit: { dim: 0.3 } } })
791
+ // popTransition = the way BACK the screen remembers (Router.pop(), the back gesture, the system's back play it).
792
+ // Default: the transition it came with, the other way round (a slide from the bottom leaves downwards;
793
+ // a zoom / a fade go back with a fade). A replace without one keeps the way back of the screen it replaces.
794
+ // A screen's own transform / opacity are not seen while a transition moves it — style a child instead.
780
795
  Router.addEventListener("change", (screen: UIScreen) => ...) / .removeEventListener("change", cb) // after every navigation
781
796
  // Screens BELOW the top stay mounted — element trees and state survive, they're just not rendered.
782
797
  // push fires the outgoing screen's onClose + the incoming one's onOpen; pop fires them in reverse and
@@ -798,7 +813,7 @@ font("ibm-plex-sans", { weights: [400, 600], italic: true }) // narrow/extend t
798
813
  // A project's own file: font("./fonts/Brand.ttf") → real family name read from the file itself
799
814
  // (weight/italic too — no options); two weight files of one family return the same name.
800
815
  // registerFont(family, url, {weight, style}) survives ONLY for runtime-computed URLs — rare.
801
- // No style inheritance — set fontFamily per UIText or via theme() (extract a shared Style<UIText>).
816
+ // The app's font: theme({ fontFamily }). A second family is set per UIText (extract a shared Style<UIText>).
802
817
 
803
818
  // ===== SVG IMAGES =====
804
819
  // SvgSource wraps raw SVG XML for use as an image source: UIImage(SvgSource(`<svg ...>`)).
@@ -900,17 +915,16 @@ screen.open()
900
915
  </file>
901
916
 
902
917
  // === EXAMPLE 2: Theme tokens + fetched list with loading / error states ===
903
- // A tokens module every screen imports, light-themed: theme({ color }) sets the default text
904
- // color ONCE — no color: "#111" on every label.
918
+ // A tokens module every screen imports, light-themed. ONE theme() call: the roles (bg, surface,
919
+ // border, text, textMuted, accent) color the screens, the default text and the tab bar — no
920
+ // color: "#111" on every label — and the app's own keys sit beside them.
905
921
  <file name="tokens.ts">
906
- const palette = {
907
- bg: "#F4F6F5", card: "#FFFFFF", border: "#E4E8E6",
908
- text: "#131A17", muted: "#606B65",
909
- accent: "#15A34A", accentSoft: "#E7F6ED", onAccent: "#FFFFFF",
910
- }
911
- theme({ color: palette.text, primaryColor: palette.accent })
912
922
  // Accessors ARE "var(--x)" strings — re-calling theme() with new values restyles the live app.
913
- export const colors: { [K in keyof typeof palette]: string } = theme(palette)
923
+ export const colors = theme({
924
+ bg: "#F4F6F5", surface: "#FFFFFF", border: "#E4E8E6",
925
+ text: "#131A17", textMuted: "#606B65", accent: "#15A34A",
926
+ accentSoft: "#E7F6ED", onAccent: "#FFFFFF",
927
+ })
914
928
  export const font = { // type scale — spread into styles: .style({ ...font.h2 })
915
929
  h2: { fontSize: 22, fontWeight: 700 }, bodyStrong: { fontSize: 16, fontWeight: 600 },
916
930
  small: { fontSize: 14 }, tiny: { fontSize: 12, fontWeight: 500 },
@@ -927,9 +941,9 @@ const Row = (u: User) => UIRow(
927
941
  justifyContent: "center", alignItems: "center" }),
928
942
  UIColumn(
929
943
  UIText(u.name).style({ ...font.bodyStrong }),
930
- UIText(u.email).style({ ...font.small, color: colors.muted }),
944
+ UIText(u.email).style({ ...font.small, color: colors.textMuted }),
931
945
  ).style({ flexGrow: 1, flexShrink: 1, gap: 2, alignItems: "flex-start" }),
932
- ).style({ alignItems: "center", gap: 12, bgColor: colors.card, borderRadius: 16,
946
+ ).style({ alignItems: "center", gap: 12, bgColor: colors.surface, borderRadius: 16,
933
947
  border: `1px solid ${colors.border}`, p: 14 })
934
948
 
935
949
  const Centered = (...children: UINodeChild[]) =>
@@ -938,7 +952,7 @@ const Centered = (...children: UINodeChild[]) =>
938
952
  let body: UIColumn
939
953
 
940
954
  const loadData = async () => {
941
- body.setContent([Centered(UIText("Loading…").style({ color: colors.muted }))])
955
+ body.setContent([Centered(UIText("Loading…").style({ color: colors.textMuted }))])
942
956
  const res = await fetch("https://jsonplaceholder.typicode.com/users")
943
957
  if (res.status !== 200) {
944
958
  body.setContent([Centered(
@@ -92,7 +92,7 @@ const label: Style<UIText> = { fontSize: 12, fontWeight: 600, letterSpacing: 2 }
92
92
  // Padding: p, px, py, pt, pb, pl, pr (≡ padding, paddingHorizontal, paddingVertical, paddingTop, ...)
93
93
  // Margin: m, mx, my, mt, mb, ml, mr — margins also accept "auto" (mx: "auto" centers a fixed-width element)
94
94
  // Background (containers/screen/button; UIText/UIImage/UISpacer have bgColor only):
95
- // bgColor, bgImage (string | imported asset), bgSize ("cover"|"contain"|"tile"), bgGradient
95
+ // bgColor, bgImage (string | imported asset), bgSize ("cover"|"contain"|"fill"|"tile"), bgGradient
96
96
  // bgGradient: a CSS linear-gradient() or radial-gradient() string, e.g. "linear-gradient(to top, rgba(0,0,0,0.7), transparent)".
97
97
  // Layers paint bottom-to-top: bgColor → bgImage → bgGradient (gradient over image = text scrim).
98
98
  // bgImage is decoration BEHIND children — an image that IS the content belongs in UIImage.
@@ -122,10 +122,11 @@ const label: Style<UIText> = { fontSize: 12, fontWeight: 600, letterSpacing: 2 }
122
122
 
123
123
  // --- Theme variables ---
124
124
  // theme({...}) merges app vars into one table; styles read them with "var(--name)" (numbers are px):
125
- const T = theme({ primaryColor: "#0A84FF" })
126
- label.style({ color: T.primaryColor }) // or "var(--primaryColor)"
127
- // theme({ color, fontFamily }) drive the DEFAULT text color/font app-wide. "var(--name, fallback)"
128
- // applies the fallback while the key is unset — how the SDK tab bar (defineTabs) stays themeable.
125
+ const T = theme({ accent: "#0A84FF" })
126
+ label.style({ color: T.accent }) // or "var(--accent)"
127
+ // ROLES the SDK reads itself: bg (screens) · surface / border (the tab bar) · text / fontFamily
128
+ // (text that names none) · textMuted (inactive tabs) · accent (the active tab). "var(--name,
129
+ // fallback)" applies the fallback while the key is unset.
129
130
 
130
131
  // --- Defaults that surprise ---
131
132
  // flexShrink: 0 — elements don't shrink to fit (exception: UIScrollable defaults flexShrink: 1 so it scrolls
package/prompts/ui.md CHANGED
@@ -42,8 +42,8 @@ Router.init(tabs) // UITabs IS a UIScreen — present it directly
42
42
  // .select(id), .tab (getter), .onSelect(cb(id, i)) — fires on a bar tap, swipe, or select()
43
43
  // .badge(id, value) — true = dot, number/string = count pill, false/null/0 clears
44
44
  // .pager — the UIPager underneath; UIPager.push(detail) from any screen keeps the bar
45
- // Styling is THEME-driven: theme({ primaryColor, mutedColor, tabbarBg, tabbarBorder, badgeColor })
46
- // restyles the bar app-wide (dark fallbacks built in). For a custom bar layout use UIPager below.
45
+ // The bar is colored by the theme's roles (accent, textMuted, surface, border) — no keys of its
46
+ // own; one bar's own colors: tabs.bar.theme({ accent, surface }). Custom layout: UIPager below.
47
47
 
48
48
  // UIPager — the navigation primitive under UITabs: sibling tabs that swipe natively, each tab its
49
49
  // OWN push/pop stack. Reach for it directly for a plain stack (one-screen pager) or a fully custom
@@ -259,7 +259,7 @@ const btn: Style<UIButton> = { bgColor: "#333", borderRadius: 12 }
259
259
  // Padding: p, px, py, pt, pb, pl, pr (≡ padding, paddingHorizontal, paddingVertical, paddingTop, ...)
260
260
  // Margin: m, mx, my, mt, mb, ml, mr — margins also accept "auto" (mx: "auto" centers a fixed-width element)
261
261
  // Background (containers/screen/button/input/video; UIText/UIImage/UISpacer have bgColor only):
262
- // bgColor, bgImage (string | FetchResponse | File), bgSize ("cover"|"contain"|"tile"), bgGradient
262
+ // bgColor, bgImage (string | FetchResponse | File | SvgSource), bgSize ("cover"|"contain"|"fill"|"tile"), bgGradient
263
263
  // bgGradient: a CSS linear-gradient() or radial-gradient() string, e.g. "linear-gradient(to top, rgba(0,0,0,0.7), transparent)"
264
264
  // or "radial-gradient(circle at 50% 40%, #7B2FF7, #0a0a1a)". Comma-separate several gradients to stack them (first = on top).
265
265
  // Layers paint bottom-to-top: bgColor → bgImage → bgGradient (gradient over image = text scrim).
@@ -298,11 +298,15 @@ const btn: Style<UIButton> = { bgColor: "#333", borderRadius: 12 }
298
298
  // --- Theme variables ---
299
299
  // theme({...}) merges app vars into one table; styles read them with "var(--name)" (numbers are px
300
300
  // lengths, null removes a key). Re-calling re-styles the LIVE UI — dark mode is a second call.
301
- // Returned accessors ARE the var() strings; system vars are statics (theme.primaryColor, …):
302
- const T = theme({ primaryColor: "#0A84FF", "comfort-left": 20 })
303
- label.style({ color: T.primaryColor, pl: "var(--comfort-left)" })
304
- // SYSTEM DEFAULTS: theme({ color, fontFamily }) drive the DEFAULT text color/font — all text
305
- // without explicit values follows (light theme = set once, not color:"black" per label).
301
+ // ONE call per app. Returned accessors ARE the var() strings:
302
+ const T = theme({ accent: "#0A84FF", "comfort-left": 20 })
303
+ label.style({ color: T.accent, pl: "var(--comfort-left)" })
304
+ // ROLES — keys the SDK reads itself; use them as your palette and add your own keys beside them:
305
+ // bg (every screen's background) · surface (cards, bars; the tab bar) · border (hairlines)
306
+ // text (text with no color of its own) · textMuted (secondary; inactive tabs) · accent (active tab)
307
+ // fontFamily (text with no font of its own). Light theme = set text once, not a color per label.
308
+ // SCOPE: el.theme({...}) — the same keys for ONE subtree (a card, tabs.bar, a whole screen); its
309
+ // descendants read them first. A pushed screen / a widget is its own root: give it its own.
306
310
  // Bare comfort-* tokens apply the safe-area formula; "var(--comfort-top)" reads the raw knob.
307
311
  // "var(--name, fallback)" applies the fallback while the key is unset. Env names (safe-*, vw/vh) are not theme keys.
308
312
 
@@ -334,7 +338,7 @@ screen.style({ flexDirection: "column", p: 16, $landscape: { flexDirection: "row
334
338
  // alignItems: "stretch" — children FILL the cross axis by default; set a size or alignSelf to opt out.
335
339
  // overflow: "hidden" — children are clipped to the parent's box; set overflow: "visible" to let them escape.
336
340
  // boxSizing: "border-box" — width/height include padding & border.
337
- // Screen bg is black, default text is WHITE at fontSize 14 (retarget once via theme({ color, fontFamily })).
341
+ // Screen bg is black, default text is WHITE at fontSize 14 (retarget once via theme({ bg, text, fontFamily })).
338
342
  // On any light surface — white cards, sheets, inputs — white text renders invisible: use dark text there
339
343
  // ("#111"/black), checking every UIText against the surface it actually sits on, not the screen.
340
344
 
@@ -448,7 +452,7 @@ font("ibm-plex-sans", { weights: [400, 600], italic: true }) // narrow/extend t
448
452
  // A project's own file: font("./fonts/Brand.ttf") → real family name read from the file itself
449
453
  // (weight/italic too — no options); two weight files of one family return the same name.
450
454
  // registerFont(family, url, {weight, style}) survives ONLY for runtime-computed URLs — rare.
451
- // No style inheritance — set fontFamily per UIText or via theme() (extract a shared Style<UIText>).
455
+ // The app's font: theme({ fontFamily }). A second family is set per UIText (extract a shared Style<UIText>).
452
456
 
453
457
  // ===== SVG IMAGES =====
454
458
  // SvgSource wraps raw SVG XML for use as an image source: UIImage(SvgSource(`<svg ...>`)).
@@ -550,17 +554,16 @@ screen.open()
550
554
  </file>
551
555
 
552
556
  // === EXAMPLE 2: Theme tokens + fetched list with loading / error states ===
553
- // A tokens module every screen imports, light-themed: theme({ color }) sets the default text
554
- // color ONCE — no color: "#111" on every label.
557
+ // A tokens module every screen imports, light-themed. ONE theme() call: the roles (bg, surface,
558
+ // border, text, textMuted, accent) color the screens, the default text and the tab bar — no
559
+ // color: "#111" on every label — and the app's own keys sit beside them.
555
560
  <file name="tokens.ts">
556
- const palette = {
557
- bg: "#F4F6F5", card: "#FFFFFF", border: "#E4E8E6",
558
- text: "#131A17", muted: "#606B65",
559
- accent: "#15A34A", accentSoft: "#E7F6ED", onAccent: "#FFFFFF",
560
- }
561
- theme({ color: palette.text, primaryColor: palette.accent })
562
561
  // Accessors ARE "var(--x)" strings — re-calling theme() with new values restyles the live app.
563
- export const colors: { [K in keyof typeof palette]: string } = theme(palette)
562
+ export const colors = theme({
563
+ bg: "#F4F6F5", surface: "#FFFFFF", border: "#E4E8E6",
564
+ text: "#131A17", textMuted: "#606B65", accent: "#15A34A",
565
+ accentSoft: "#E7F6ED", onAccent: "#FFFFFF",
566
+ })
564
567
  export const font = { // type scale — spread into styles: .style({ ...font.h2 })
565
568
  h2: { fontSize: 22, fontWeight: 700 }, bodyStrong: { fontSize: 16, fontWeight: 600 },
566
569
  small: { fontSize: 14 }, tiny: { fontSize: 12, fontWeight: 500 },
@@ -577,9 +580,9 @@ const Row = (u: User) => UIRow(
577
580
  justifyContent: "center", alignItems: "center" }),
578
581
  UIColumn(
579
582
  UIText(u.name).style({ ...font.bodyStrong }),
580
- UIText(u.email).style({ ...font.small, color: colors.muted }),
583
+ UIText(u.email).style({ ...font.small, color: colors.textMuted }),
581
584
  ).style({ flexGrow: 1, flexShrink: 1, gap: 2, alignItems: "flex-start" }),
582
- ).style({ alignItems: "center", gap: 12, bgColor: colors.card, borderRadius: 16,
585
+ ).style({ alignItems: "center", gap: 12, bgColor: colors.surface, borderRadius: 16,
583
586
  border: `1px solid ${colors.border}`, p: 14 })
584
587
 
585
588
  const Centered = (...children: UINodeChild[]) =>
@@ -588,7 +591,7 @@ const Centered = (...children: UINodeChild[]) =>
588
591
  let body: UIColumn
589
592
 
590
593
  const loadData = async () => {
591
- body.setContent([Centered(UIText("Loading…").style({ color: colors.muted }))])
594
+ body.setContent([Centered(UIText("Loading…").style({ color: colors.textMuted }))])
592
595
  const res = await fetch("https://jsonplaceholder.typicode.com/users")
593
596
  if (res.status !== 200) {
594
597
  body.setContent([Centered(
@@ -28,6 +28,15 @@ declare global {
28
28
  /** Fire a one-shot semantic haptic (device.vibrate). `style` is a HapticStyle string. Optional:
29
29
  * hosts without haptic hardware (web, headless, iPad / older iPhones) simply don't implement it. */
30
30
  vibrate?(style: string): void
31
+ /** The HOST's performance overlay (device.statsOverlay): frames per second as the display got
32
+ * them, how evenly, the GPU's and the main thread's milliseconds — a line the host draws over
33
+ * everything itself, about once a second, with no JS, tree or layout behind it. It is the
34
+ * host's state, not a world's: it stays through `_creatorApp.run()`, so a launcher turns it
35
+ * on for whatever it opens next. Optional, as a pair: a host with no overlay (web, headless)
36
+ * leaves both out and the SDK reads `false`. */
37
+ setStatsOverlay?(enabled: boolean): void
38
+ /** Whether the overlay is on. */
39
+ statsOverlay?(): boolean
31
40
 
32
41
  // ---- device.motion (orientation sensor; pull-per-frame model) --------------------------------
33
42
  /** Does this device have the motion sensors (gyro)? Surfaced as device.motion.available. */
@@ -67,6 +67,11 @@ declare global {
67
67
  // Theme variables: MERGES `values` (null removes a key, numbers are px lengths, a color is a
68
68
  // string) and re-resolves live styles. Env names (safe-*, vw/vh/…) are reserved.
69
69
  setTheme(values: Record<string, string | f64 | null>): void
70
+ // The same for ONE node's subtree (`el.theme({...})`): a SCOPE. A `var(--name)` on a node reads
71
+ // the nearest scope up its tree — the node, its ancestors, and for a pager's page or a vlist's
72
+ // item the pager / list it is in — then the app's table. A destination pushed over the node and
73
+ // a widget are roots of their own: no scope reaches them. The comfort-* knobs stay app-wide.
74
+ setNodeTheme(id: TreeNodeId, values: Record<string, string | f64 | null>): void
70
75
 
71
76
  // ---- presentation -------------------------------------------------------------------------
72
77
  // A transition (sdk/src/ui/transitions.ts): a tween blob whose targets are ROLES — `roles[i]` is
package/src/chisel.ts CHANGED
@@ -123,7 +123,7 @@ export const chiselVocab: ChiselVocab = {
123
123
  // Exactly the builder methods that return the node (tests/compile/chisel-vocab.test.ts proves it against
124
124
  // the interfaces): `animateTo` / `animateFrom` return an Animation, so they are NOT here.
125
125
  chain: [
126
- "style", "class", "named",
126
+ "style", "class", "named", "theme",
127
127
  "onClick", "onTouchStart", "onLongPress", "onMouseEnter", "onLayout", "onOpen", "onClose", "onBack",
128
128
  "onScroll", "onScrollRelease", "onOverscroll", "onRefresh", "onSubmit",
129
129
  "onSelect", "onChange", "onFocus", "onBlur", "onOverlayTap", "onDetent",
@@ -69,6 +69,12 @@ export const SERVER_INJECT = "/__sdk/server/inject.ts"
69
69
  * contract, `__sceneEditor` (src/inject.editor.ts → src/scene/editor.ts). Never in a shipped bundle. */
70
70
  export const EDITOR_INJECT = "/__sdk/inject.editor.ts"
71
71
 
72
+ /** The text of the two inject modules — the lists of what is a global on each side (serverTypes.ts). */
73
+ export const readInjectSources = (): { client: string, server: string } => {
74
+ const client = resolveInject()
75
+ return { client: readFileSync(client, "utf8"), server: readFileSync(join(dirname(client), "server", "inject.ts"), "utf8") }
76
+ }
77
+
72
78
  /** The SDK source root (parent of `inject.ts`). chisel needs the source tree, not a prebuilt bundle. */
73
79
  const resolveSdkSrc = (): string => {
74
80
  const inject = resolveInject()
@@ -7,7 +7,7 @@ import { bundleProjectWithMap, scanModulesChisel, type MemoryFiles } from "./bun
7
7
  import { detectEntry } from "./detectEntry"
8
8
  import { buildHeader } from "./header"
9
9
  import { inlineSourceMapComment, offsetSourceMap } from "./sourcemap"
10
- import { stubServerEntries } from "./serverSplit"
10
+ import { ClientPublishError, clientPublishes, stubServerEntries } from "./serverSplit"
11
11
 
12
12
  export type CompileFileType = "text" | "resource" | "shader"
13
13
 
@@ -185,6 +185,8 @@ export const pluginsOfBundle = (mapJson: string): string[] => {
185
185
 
186
186
  export const compileProjectWithMap = async (opts: CompileOptions): Promise<CompileResult> => {
187
187
  // Client bundle: every *.server.ts becomes its RPC/channel stub (server code stays server-side).
188
+ const published = clientPublishes(opts.entries)
189
+ if (published.length) throw new ClientPublishError(published)
188
190
  const entries = stubServerEntries(opts.entries, opts.serverUrl ?? "")
189
191
  const entry = opts.entryOverride ?? detectEntry(entries, scanModulesChisel).entry
190
192
  if (!entry) {
@@ -4,6 +4,8 @@
4
4
  export { bundleProject, bundleProjectWithMap, scanModulesChisel, CLIENT_INJECT, SERVER_INJECT, type MemoryFiles } from "./bundler"
5
5
  export {
6
6
  compileServerBundle,
7
+ clientPublishes,
8
+ ClientPublishError,
7
9
  scanServerExports,
8
10
  scanServerModules,
9
11
  stubServerEntries,
@@ -17,7 +19,7 @@ export {
17
19
  type ServerExportKind,
18
20
  type ServerManifest,
19
21
  } from "./serverSplit"
20
- export { deriveEndpointParams, EndpointTypeError, type DeriveResult } from "./serverTypes"
22
+ export { deriveEndpointParams, EndpointTypeError, SyncEndpointError, UnawaitedUserQueryError, WrongSideGlobalError, type DeriveResult } from "./serverTypes"
21
23
  export {
22
24
  detectEntry,
23
25
  scanModulesFallback,
@@ -9,7 +9,7 @@
9
9
  * side effects, not entry exports) — the runner reads that (src/server/runtime.ts).
10
10
  *
11
11
  * Export kinds are found by a static scan (no TS program needed): a function export is an
12
- * endpoint, a `channel(...)` export is a channel, anything else is server-only.
12
+ * endpoint, a `channel<…>()…` export is a channel, anything else is server-only.
13
13
  */
14
14
 
15
15
  import type { AppConfig } from "../server/auth/appConfig"
@@ -129,25 +129,70 @@ export const serverStubSource = (path: string, exports: ServerExport[], serverUr
129
129
  return `// client stub of ${mod} — generated by the lecodes compiler\n${lines.join("\n")}\n`
130
130
  }
131
131
 
132
+ // ───────────────────────────── a channel in the app ─────────────────────────────
133
+
134
+ /** A channel export has ONE type on both sides, so `publish` type-checks in the app — where the
135
+ * export is the subscribing proxy. Said at compile time, with what to do instead. */
136
+ export class ClientPublishError extends Error {
137
+ readonly problems: string[]
138
+ constructor(problems: string[]) {
139
+ super(`a channel goes from the server to the app — the app subscribes; to send, call a server function that publishes:\n ${problems.join("\n ")}`)
140
+ this.name = "ClientPublishError"
141
+ this.problems = problems
142
+ }
143
+ }
144
+
145
+ /** `<channel>.publish(` in the project's client files, for every channel imported from a `*.server.ts`. */
146
+ export const clientPublishes = (entries: CompileEntry[]): string[] => {
147
+ const channels = new Map<string, Set<string>>()
148
+ for (const [path, exports] of Object.entries(scanServerModules(entries))) {
149
+ const names = exports.filter(e => e.kind === "channel").map(e => e.name)
150
+ if (names.length) channels.set(path.replace(/\.[tj]s$/, ""), new Set(names))
151
+ }
152
+ if (channels.size === 0) return []
153
+ const out: string[] = []
154
+ for (const e of entries) {
155
+ if (e.type !== "text" || isServerPath(e.path) || !/\.[tj]sx?$/.test(e.path)) continue
156
+ const src = e.text ?? ""
157
+ const dir = normalizePath(e.path).replace(/\/[^/]*$/, "")
158
+ const locals: string[] = []
159
+ for (const m of src.matchAll(/import\s*\{([^}]*)\}\s*from\s*["']([^"']+)["']/g)) {
160
+ if (!m[2].startsWith(".")) continue
161
+ const names = channels.get(normalizePath(`${dir}/${m[2]}`).replace(/\.[tj]s$/, ""))
162
+ if (!names) continue
163
+ for (const part of m[1].split(",")) {
164
+ const [imported, local] = part.split(/\s+as\s+/).map(s => s.trim())
165
+ if (imported && names.has(imported)) locals.push(local ?? imported)
166
+ }
167
+ }
168
+ if (!locals.length) continue
169
+ const code = blankCommentsAndStrings(src)
170
+ for (const local of locals) {
171
+ for (const m of code.matchAll(new RegExp(`(?<![\\w$.])${local.replace(/\$/g, "\\$")}\\s*\\.\\s*publish\\s*\\(`, "g"))) {
172
+ out.push(`${serverModuleId(e.path)}:${code.slice(0, m.index).split("\n").length}: ${local}.publish(…)`)
173
+ }
174
+ }
175
+ }
176
+ return out
177
+ }
178
+
132
179
  // ───────────────────────────── server entry ─────────────────────────────
133
180
 
134
181
  export const SERVER_ENTRY_PATH = "/.server-entry.ts"
135
182
  /** The SDK's app-config placeholder (src/server/auth/appConfig.ts) — the compile overrides it in the file map
136
- * with the project's real values (project files win over SDK files), so `auth.model.user()` and friends see
183
+ * with the project's real values (project files win over SDK files), so an SDK module that imports it sees
137
184
  * app.json as an ordinary import. */
138
185
  export const APP_CONFIG_PATH = "/__sdk/server/auth/appConfig.ts"
139
186
 
140
187
  /** Compile-time manifest embedded in the server bundle (validators land here — see serverTypes.ts). */
141
188
  export type ServerManifest = {
142
189
  modules: Record<string, ServerExport[]>
143
- /** endpoint id → JSON-schema-ish param list (filled by the TS-checker pass; absent = unvalidated). */
190
+ /** endpoint id → JSON-schema-ish param list (filled by the TS-checker pass; absent = unvalidated).
191
+ * A channel id → the parameters of its `groupBy` (what the app passes to `subscribe`). */
144
192
  params?: Record<string, unknown[]>
145
- /** The `app.json` subset the server runtime reads (name, auth providers); the bundle itself gets it as the
193
+ /** The `app.json` subset the server runtime reads (the app's name); the bundle itself gets it as the
146
194
  * compiled-in `appConfig` module (APP_CONFIG_PATH). */
147
195
  app?: AppConfig
148
- /** Some server file references the `auth` global: the runner then resolves a device session on every
149
- * request (minting one on first contact). Projects that never touch `auth` pay nothing. */
150
- usesAuth?: boolean
151
196
  }
152
197
 
153
198
  /** The synthetic entry: imports every export by name (chisel has no namespace imports) and registers
@@ -196,12 +241,15 @@ export type CompileServerOptions = {
196
241
  /** Derive per-endpoint argument validators from the TS parameter types (serverTypes.ts) and embed
197
242
  * them in the manifest. Default true; throws `EndpointTypeError` on non-JSON parameter types. */
198
243
  validate?: boolean
244
+ /** The TypeScript the validators are derived with, for a caller that brings its own (the CLI takes
245
+ * the project's); absent = the `typescript` package this module can import. */
246
+ typescript?: typeof import("typescript")
199
247
  /** Precomputed param schemas to embed instead of deriving them. */
200
248
  params?: Record<string, unknown[]>
201
249
  /** Server bundles are readable by default (stack traces in the server console). */
202
250
  minify?: boolean
203
251
  sourcemap?: boolean
204
- /** The project's `app.json` (name + auth providers) — embedded in the manifest and exposed to the bundle. */
252
+ /** The project's `app.json` subset (its name) — embedded in the manifest and exposed to the bundle. */
205
253
  app?: AppConfig
206
254
  }
207
255
 
@@ -224,12 +272,11 @@ export const compileServerBundle = async (opts: CompileServerOptions): Promise<{
224
272
  }
225
273
  let params = opts.params
226
274
  if (!params && opts.validate !== false) {
227
- const derived = await deriveEndpointParams(opts.entries, modules)
275
+ const derived = await deriveEndpointParams(opts.entries, modules, opts.typescript)
228
276
  for (const w of derived.warnings) console.warn(`warning: ${w}`)
229
277
  params = derived.params
230
278
  }
231
- const usesAuth = opts.entries.some(e => e.type === "text" && isServerPath(e.path) && /\bauth\s*\./.test(blankCommentsAndStrings(e.text ?? "")))
232
- const manifest: ServerManifest = { modules: Object.fromEntries(Object.entries(modules).map(([p, ex]) => [serverModuleId(p), ex])), params, app: opts.app, usesAuth }
279
+ const manifest: ServerManifest = { modules: Object.fromEntries(Object.entries(modules).map(([p, ex]) => [serverModuleId(p), ex])), params, app: opts.app }
233
280
  if (opts.app) files[APP_CONFIG_PATH] = `export const app = ${JSON.stringify(opts.app)}\n`
234
281
  files[SERVER_ENTRY_PATH] = serverEntrySource(modules, manifest)
235
282
  const { code } = await bundleProjectWithMap(