lecodes-sdk 2.0.10 → 2.0.12

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/package.json CHANGED
@@ -45,7 +45,7 @@
45
45
  "marcidb-embedded": "^0.14.1",
46
46
  "sharp": "^0.35.2"
47
47
  },
48
- "version": "2.0.10",
48
+ "version": "2.0.12",
49
49
  "files": [
50
50
  "src",
51
51
  "dist",
package/prompts/ui.md CHANGED
@@ -41,6 +41,8 @@ const tabs = UITabs({
41
41
  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
+ // Ambient, from any screen, no reference threading: UITabs.current (the visible tabs, or null) and
45
+ // UITabs.select("profile") — the twin of UIPager.current / UIPager.push
44
46
  // .pager — the UIPager underneath; UIPager.push(detail) from any screen keeps the bar
45
47
  // The bar is colored by the theme's roles (accent, textMuted, surface, border) — no keys of its
46
48
  // own; one bar's own colors: tabs.bar.theme({ accent, surface }). Custom layout: UIPager below.
@@ -19,7 +19,7 @@ export {
19
19
  type ServerExportKind,
20
20
  type ServerManifest,
21
21
  } from "./serverSplit"
22
- export { deriveEndpointParams, EndpointTypeError, SyncEndpointError, UnawaitedUserQueryError, WrongSideGlobalError, type DeriveResult } from "./serverTypes"
22
+ export { deriveEndpointParams, EndpointTypeError, ServerHelperImportError, UnawaitedUserQueryError, WrongSideGlobalError, type DeriveResult } from "./serverTypes"
23
23
  export {
24
24
  detectEntry,
25
25
  scanModulesFallback,
@@ -23,6 +23,12 @@ export const isServerPath = (path: string) => SERVER_FILE.test(path)
23
23
  /** Project path → endpoint-id prefix: no leading `./` or `/`, forward slashes. */
24
24
  export const serverModuleId = (path: string) => path.split("\\").join("/").replace(/^(\.\/|\/)+/, "")
25
25
 
26
+ /**
27
+ * What an export of a server module is to the app: `fn` = an endpoint — an ASYNC function, called
28
+ * over the network; `channel` = a channel; `other` = server-only — a model, the db, a constant, or
29
+ * a helper: a function that is not async is used by server files alone, and the app's import of
30
+ * it is refused at compile time (serverTypes.ts), never a call that silently holds a promise.
31
+ */
26
32
  export type ServerExportKind = "fn" | "channel" | "other"
27
33
  export type ServerExport = { name: string, kind: ServerExportKind }
28
34
 
@@ -60,18 +66,9 @@ const IDENT = "[A-Za-z_$][\\w$]*"
60
66
  const classifyInit = (init: string): ServerExportKind => {
61
67
  const s = init.trimStart()
62
68
  if (/^channel\s*[<(]/.test(s)) return "channel"
63
- if (/^(async\s+)?function\b/.test(s)) return "fn"
69
+ if (/^async\s+function\b/.test(s)) return "fn"
64
70
  if (/^async\b/.test(s)) return "fn" // async (…) => / async x =>
65
- if (new RegExp(`^${IDENT}\\s*=>`).test(s)) return "fn" // x => …
66
- if (/^\(/.test(s)) {
67
- // `(a, b) => …` — find the matching paren and look for `=>` (a type annotation may sit between)
68
- let depth = 0
69
- for (let i = 0; i < s.length; i++) {
70
- if (s[i] === "(") depth++
71
- else if (s[i] === ")") { depth--; if (depth === 0) return /^\s*(:[^=]*)?=>/.test(s.slice(i + 1)) ? "fn" : "other" }
72
- }
73
- }
74
- return "other"
71
+ return "other" // a sync function, a value: the server's own
75
72
  }
76
73
 
77
74
  /** Exported names of a server module with their kinds. Default exports are rejected. */
@@ -87,8 +84,8 @@ export const scanServerExports = (source: string, path = "<server module>"): Ser
87
84
  if (i >= 0) found[i] = { at: found[i].at, name, kind }
88
85
  else found.push({ at, name, kind })
89
86
  }
90
- // export [async] function name / export function* name
91
- for (const m of src.matchAll(new RegExp(`(^|\\n)\\s*export\\s+(async\\s+)?function\\s*\\*?\\s*(${IDENT})`, "g"))) add(m.index!, m[3], "fn")
87
+ // export async function name = an endpoint; export function name = a helper of the server
88
+ for (const m of src.matchAll(new RegExp(`(^|\\n)\\s*export\\s+(async\\s+)?function\\s*\\*?\\s*(${IDENT})`, "g"))) add(m.index!, m[3], m[2] ? "fn" : "other")
92
89
  // export const|let|var name [: T] = init
93
90
  for (const m of src.matchAll(new RegExp(`(^|\\n)\\s*export\\s+(?:const|let|var)\\s+(${IDENT})\\s*(?::[^=]*?)?=\\s*([^\\n]*)`, "g"))) {
94
91
  add(m.index!, m[2], classifyInit(m[3]))
@@ -102,7 +99,7 @@ export const scanServerExports = (source: string, path = "<server module>"): Ser
102
99
  if (!local || /^type\s/.test(local)) continue
103
100
  const name = exported ?? local
104
101
  let kind: ServerExportKind = "other"
105
- if (new RegExp(`(^|\\n)\\s*(async\\s+)?function\\s*\\*?\\s*${local}\\b`).test(src)) kind = "fn"
102
+ if (new RegExp(`(^|\\n)\\s*async\\s+function\\s*\\*?\\s*${local}\\b`).test(src)) kind = "fn"
106
103
  else {
107
104
  const decl = src.match(new RegExp(`(^|\\n)\\s*(?:const|let|var)\\s+${local}\\s*(?::[^=]*?)?=\\s*([^\\n]*)`))
108
105
  if (decl) kind = classifyInit(decl[2])
@@ -47,15 +47,16 @@ export class EndpointTypeError extends Error {
47
47
  }
48
48
 
49
49
  /**
50
- * In the app an endpoint's call is always a promise: it goes over the network. An endpoint that is
51
- * not `async` is imported with the type it was written with (`number`), so `const n = add(1, 2)`
52
- * type-checks and holds a promise. Declared `async`, the two agree.
50
+ * In the app an endpoint's call is always a promise: it goes over the network. A function of a
51
+ * server file that is not `async` is therefore not an endpoint but a helper, the server's own
52
+ * (serverSplit.ts): imported by the app it would keep the type it was written with (`number`), so
53
+ * `const n = add(1, 2)` would type-check and throw. The import is refused instead.
53
54
  */
54
- export class SyncEndpointError extends Error {
55
+ export class ServerHelperImportError extends Error {
55
56
  readonly problems: string[]
56
57
  constructor(problems: string[]) {
57
- super(`an endpoint is called over the network, so in the app its result is a promise — declare it async:\n ${problems.join("\n ")}`)
58
- this.name = "SyncEndpointError"
58
+ super(`a function of a server file that is not async is a helper of the server, for server files only — the app calls endpoints, which are \`export async function\`:\n ${problems.join("\n ")}`)
59
+ this.name = "ServerHelperImportError"
59
60
  this.problems = problems
60
61
  }
61
62
  }
@@ -135,6 +136,38 @@ export type DeriveResult = {
135
136
  warnings: string[]
136
137
  }
137
138
 
139
+ /**
140
+ * The app's imports of a server module's helpers: a named import, from a `*.server.ts` file, of an
141
+ * export the scan classed `other` that is a function. The checker resolves the import to its
142
+ * declaration, so the module is the one the import names, however it was spelled.
143
+ */
144
+ const helperImports = (ts: TS, program: import("typescript").Program, checker: import("typescript").TypeChecker, modules: Record<string, ServerExport[]>, projectFiles: Set<string>): string[] => {
145
+ const kinds = new Map(Object.entries(modules).map(([path, exports]) => [normalizePath(path), new Map(exports.map(e => [e.name, e.kind]))]))
146
+ const out: string[] = []
147
+ for (const sf of program.getSourceFiles()) {
148
+ if (!projectFiles.has(sf.fileName) || sf.isDeclarationFile || isServerPath(sf.fileName)) continue
149
+ const file = serverModuleId(sf.fileName)
150
+ for (const statement of sf.statements) {
151
+ if (!ts.isImportDeclaration(statement) || !statement.importClause?.namedBindings || !ts.isNamedImports(statement.importClause.namedBindings)) continue
152
+ if (statement.importClause.isTypeOnly) continue
153
+ for (const spec of statement.importClause.namedBindings.elements) {
154
+ if (spec.isTypeOnly) continue
155
+ let symbol = checker.getSymbolAtLocation(spec.name)
156
+ if (symbol && symbol.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol)
157
+ const decl = symbol?.valueDeclaration ?? symbol?.declarations?.[0]
158
+ if (!symbol || !decl) continue
159
+ const target = normalizePath(decl.getSourceFile().fileName)
160
+ const name = (spec.propertyName ?? spec.name).text
161
+ if (kinds.get(target)?.get(name) !== "other") continue
162
+ if (checker.getTypeOfSymbolAtLocation(symbol, decl).getCallSignatures().length === 0) continue
163
+ const { line } = sf.getLineAndCharacterOfPosition(spec.getStart(sf))
164
+ out.push(`${file}:${line + 1}: ${name} of ${serverModuleId(target)}`)
165
+ }
166
+ }
167
+ }
168
+ return out
169
+ }
170
+
138
171
  /**
139
172
  * Derive `{ "<module>#<fn>": ParamSchema[] }` for every function export of every `*.server.ts` entry.
140
173
  * Throws `EndpointTypeError` when a parameter type can't be validated at runtime.
@@ -196,11 +229,6 @@ export const deriveEndpointParams = async (entries: CompileEntry[], modules: Rec
196
229
  const params: Record<string, ParamSchema[]> = {}
197
230
  const warnings: string[] = []
198
231
  const problems: string[] = []
199
- const sync: string[] = []
200
- // a promise, or anything awaited the same way (a query handed back as it is); `any` is not judged
201
- const awaitable = (type: import("typescript").Type): boolean =>
202
- !!(type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown))
203
- || (type.flags & ts.TypeFlags.Union ? (type as import("typescript").UnionType).types.every(awaitable) : !!type.getProperty("then"))
204
232
 
205
233
  for (const [path, exports] of Object.entries(modules)) {
206
234
  const sf = program.getSourceFile(normalizePath(path))
@@ -229,10 +257,6 @@ export const deriveEndpointParams = async (entries: CompileEntry[], modules: Rec
229
257
  }
230
258
  const sig = type.getCallSignatures()[0]
231
259
  if (!sig) { warnings.push(`${where}: not callable — unvalidated`); continue }
232
- if (ex.kind === "fn" && !awaitable(checker.getReturnTypeOfSignature(sig))) {
233
- const { line } = sf.getLineAndCharacterOfPosition(decl.getStart(sf))
234
- sync.push(`${serverModuleId(path)}:${line + 1}: ${ex.name} — write \`async\` before it`)
235
- }
236
260
  const list: ParamSchema[] = []
237
261
  for (const p of sig.getParameters()) {
238
262
  const pd = p.valueDeclaration as import("typescript").ParameterDeclaration | undefined
@@ -257,9 +281,10 @@ export const deriveEndpointParams = async (entries: CompileEntry[], modules: Rec
257
281
  }
258
282
  }
259
283
  if (problems.length) throw new EndpointTypeError(problems)
260
- if (sync.length) throw new SyncEndpointError(sync)
261
284
  const wrongSide = wrongSideGlobals(ts, program, checker, new Set(files.keys()))
262
285
  if (wrongSide.length) throw new WrongSideGlobalError(wrongSide)
286
+ const helpers = helperImports(ts, program, checker, modules, new Set(files.keys()))
287
+ if (helpers.length) throw new ServerHelperImportError(helpers)
263
288
  const unawaited: string[] = []
264
289
  for (const path of Object.keys(modules)) {
265
290
  const sf = program.getSourceFile(normalizePath(path))
package/src/ui/UIPager.ts CHANGED
@@ -31,6 +31,8 @@ export type UIPagerStyle = ElementStyle & DrawableStyle
31
31
  /** The pagers whose screen is currently visible, outermost → innermost. `UIPager.current` is the
32
32
  * last (innermost). Maintained by each owned screen's onOpen/onClose via _activate/_deactivate. */
33
33
  const _activeStack: PagerElement[] = []
34
+ /** @internal The active pagers, outermost → innermost — what `UITabs.current` searches. */
35
+ export const _activePagers = (): readonly PagerElement[] => _activeStack
34
36
 
35
37
  /**
36
38
  * The screen-navigation element: sibling **tabs** that swipe natively side to side, each with its
package/src/ui/UITabs.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { PagerElement, type UIPager } from "./UIPager"
1
+ import { PagerElement, _activePagers, type UIPager } from "./UIPager"
2
2
  import { ScreenElement, UIScreen } from "./UIScreen"
3
3
  import { UIButton } from "./UIButton"
4
4
  import { UIRow, UIColumn, type UIRow as UIRowType } from "./UIContainer"
@@ -63,6 +63,9 @@ export interface UITabs extends UIScreen {
63
63
  readonly bar: UIRowType,
64
64
  }
65
65
 
66
+ /** The tabs by the pager each owns — what `UITabs.current` resolves through the active pagers. */
67
+ const _byPager = new WeakMap<PagerElement, TabsElement>()
68
+
66
69
  export class TabsElement extends ScreenElement {
67
70
  private readonly ids: string[]
68
71
  private readonly _pagerEl: PagerElement
@@ -80,6 +83,7 @@ export class TabsElement extends ScreenElement {
80
83
  if (this.ids.length === 0) console.warn("UITabs: no tabs declared")
81
84
 
82
85
  this._pagerEl = new PagerElement({ flexGrow: 1 }, this.ids.map((id) => defs[id].screen))
86
+ _byPager.set(this._pagerEl, this)
83
87
 
84
88
  const buttons = this.ids.map((id, i) => {
85
89
  const def = defs[id]
@@ -173,10 +177,44 @@ export class TabsElement extends ScreenElement {
173
177
  }
174
178
  }
175
179
 
176
- /** Create the standard bottom-tab shell — see {@link UITabs}. Keys are tab ids in tab order;
177
- * each tab needs a `label` and its root `screen` (plus an optional `icon`). */
178
- export function UITabs(tabs: Record<string, UITabDef>): UITabs {
179
- return new TabsElement(tabs) as unknown as UITabs
180
+ export interface UITabsConstructor {
181
+ /** Create the standard bottom-tab shell — see {@link UITabs}. Keys are tab ids in tab order;
182
+ * each tab needs a `label` and its root `screen` (plus an optional `icon`). */
183
+ (tabs: Record<string, UITabDef>): UITabs
184
+ /** The tabs whose page is currently visible, or `null` when the visible screen is not a tab
185
+ * of any — the twin of `UIPager.current`, so a screen switches tabs with no reference
186
+ * threading: `UITabs.current?.select("services")`. With tabs nested inside a tab's page,
187
+ * resolves to the innermost. */
188
+ readonly current: UITabs | null
189
+ /** `UITabs.current.select(id)` — switch the visible tabs to `id`; a console warning and no-op
190
+ * when no tabs are on screen. */
191
+ select(id: string, animated?: boolean): void
192
+ }
193
+
194
+ const tabsFactory = (tabs: Record<string, UITabDef>): UITabs => new TabsElement(tabs) as unknown as UITabs
195
+
196
+ /** Create the standard bottom-tab shell — see {@link UITabs}; `UITabs.current` / `UITabs.select(id)`
197
+ * reach the visible tabs from anywhere. */
198
+ export const UITabs = tabsFactory as unknown as UITabsConstructor
199
+
200
+ const _resolveCurrent = (): TabsElement | null => {
201
+ const pagers = _activePagers()
202
+ for (let i = pagers.length - 1; i >= 0; i--) {
203
+ const tabs = _byPager.get(pagers[i])
204
+ if (tabs) return tabs
205
+ }
206
+ return null
207
+ }
208
+
209
+ // The ambient statics, attached the way UIPager's are (a statement that mutates a binding this
210
+ // module declares is kept by chisel iff `UITabs` is reached — see UIPager.ts).
211
+ Object.defineProperty(UITabs, "current", {
212
+ get: () => (_resolveCurrent() ?? null) as unknown as UITabs | null,
213
+ })
214
+ ;(UITabs as any).select = (id: string, animated?: boolean) => {
215
+ const tabs = _resolveCurrent()
216
+ if (!tabs) { console.warn("UITabs.select: no tabs on the current screen"); return }
217
+ tabs.select(id, animated)
180
218
  }
181
219
 
182
220
  // ---- defineTabs — the design-stage twin ---------------------------------------------------------
package/src/version.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  // same manifest). Its MAJOR is the bundle ↔ runtime contract: a bundle's `// sdk:` header line
5
5
  // and a host's embedded version must agree on the major for the host to run the bundle; minor
6
6
  // and patch never gate anything (features are detected, never versioned).
7
- export const SDK_VERSION = "2.0.10"
7
+ export const SDK_VERSION = "2.0.12"
8
8
 
9
9
  /** The major of a semver string, or null when it is not one. */
10
10
  export const sdkMajor = (version: string | null | undefined): number | null => {