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.
- package/README.md +104 -76
- package/dist/global.d.ts +2 -5
- package/dist/host.d.ts +3 -0
- package/dist/types/inject.d.ts +3 -3
- package/dist/types/runtime/device.d.ts +7 -0
- package/dist/types/runtime/rpc.d.ts +11 -17
- package/dist/types/runtime/wire.d.ts +53 -0
- package/dist/types/server/auth/api.d.ts +42 -0
- package/dist/types/server/auth/appConfig.d.ts +1 -5
- package/dist/types/server/auth/models.d.ts +119 -70
- package/dist/types/server/auth/types.d.ts +19 -43
- package/dist/types/server/channel.d.ts +57 -19
- package/dist/types/server/context.d.ts +2 -2
- package/dist/types/server/db/defineDb.d.ts +10 -0
- package/dist/types/server/db/index.d.ts +1 -1
- package/dist/types/server/db/types.d.ts +76 -6
- package/dist/types/server/inject.d.ts +0 -1
- package/dist/types/ui/UINode.d.ts +19 -5
- package/dist/types/ui/UIScreen.d.ts +1 -0
- package/dist/types/ui/UITabs.d.ts +8 -6
- package/dist/types/ui/theme.d.ts +48 -13
- package/dist/types/version.d.ts +1 -1
- package/dist/types.json +1 -1
- package/package.json +3 -2
- package/prompts/README.md +1 -1
- package/prompts/design.md +19 -19
- package/prompts/dist/2d-game.md +45 -31
- package/prompts/dist/3d-app.md +45 -31
- package/prompts/dist/ar-app.md +45 -31
- package/prompts/dist/design.md +25 -24
- package/prompts/dist/ui-app.md +45 -31
- package/prompts/ui-design.md +6 -5
- package/prompts/ui.md +25 -22
- package/src/bridges/device.d.ts +9 -0
- package/src/bridges/tree.d.ts +5 -0
- package/src/chisel.ts +1 -1
- package/src/compile/bundler.ts +6 -0
- package/src/compile/compileProject.ts +3 -1
- package/src/compile/index.ts +3 -1
- package/src/compile/serverSplit.ts +58 -11
- package/src/compile/serverTypes.ts +189 -8
- package/src/host.d.ts +3 -0
- package/src/inject.ts +6 -6
- package/src/runtime/device.ts +12 -0
- package/src/runtime/rpc.ts +101 -40
- package/src/runtime/wire.ts +35 -0
- package/src/server/auth/api.ts +94 -0
- package/src/server/auth/appConfig.ts +2 -3
- package/src/server/auth/host.ts +244 -174
- package/src/server/auth/models.ts +45 -62
- package/src/server/auth/types.ts +19 -34
- package/src/server/channel.ts +97 -29
- package/src/server/channelHub.ts +153 -0
- package/src/server/context.ts +2 -2
- package/src/server/db/defineDb.ts +96 -36
- package/src/server/db/index.ts +1 -1
- package/src/server/db/types.ts +76 -8
- package/src/server/host.ts +25 -10
- package/src/server/inject.ts +2 -2
- package/src/server/runtime.ts +34 -12
- package/src/ui/UINode.ts +22 -5
- package/src/ui/UIScreen.ts +5 -0
- package/src/ui/UITabs.ts +19 -17
- package/src/ui/styleColor.ts +10 -1
- package/src/ui/theme.ts +96 -41
- package/src/version.ts +1 -1
- package/tests/helpers/fakeTree.ts +1 -0
- package/dist/types/plugins/oauth.d.ts +0 -25
- package/dist/types/server/auth/global.d.ts +0 -56
- package/src/plugins/oauth.ts +0 -61
- package/src/server/auth/global.ts +0 -80
- package/tests/helpers/memoryMarci.ts +0 -124
package/prompts/dist/design.md
CHANGED
|
@@ -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({
|
|
225
|
-
label.style({ color: T.
|
|
226
|
-
//
|
|
227
|
-
//
|
|
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)
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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", [...])`.
|
|
483
|
-
`
|
|
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
|
|
package/prompts/dist/ui-app.md
CHANGED
|
@@ -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
|
-
//
|
|
407
|
-
//
|
|
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
|
|
651
|
-
// knob ADDS instead. Knob defaults: 12 top,
|
|
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,
|
|
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
|
|
663
|
-
const T = theme({
|
|
664
|
-
label.style({ color: T.
|
|
665
|
-
//
|
|
666
|
-
//
|
|
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({
|
|
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)
|
|
773
|
-
Router.pop(to?: number
|
|
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
|
|
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
|
-
//
|
|
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
|
|
904
|
-
//
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
package/prompts/ui-design.md
CHANGED
|
@@ -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({
|
|
126
|
-
label.style({ color: T.
|
|
127
|
-
//
|
|
128
|
-
//
|
|
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
|
-
//
|
|
46
|
-
//
|
|
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
|
|
302
|
-
const T = theme({
|
|
303
|
-
label.style({ color: T.
|
|
304
|
-
//
|
|
305
|
-
//
|
|
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({
|
|
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
|
-
//
|
|
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
|
|
554
|
-
//
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
package/src/bridges/device.d.ts
CHANGED
|
@@ -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. */
|
package/src/bridges/tree.d.ts
CHANGED
|
@@ -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",
|
package/src/compile/bundler.ts
CHANGED
|
@@ -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) {
|
package/src/compile/index.ts
CHANGED
|
@@ -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(
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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(
|