lecodes-sdk 2.0.11 → 2.0.13
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/dist/types/version.d.ts +1 -1
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/src/compile/index.ts +1 -1
- package/src/compile/serverSplit.ts +11 -14
- package/src/compile/serverTypes.ts +41 -16
- package/src/server/db/defineDb.ts +1 -0
- package/src/server/db/types.ts +5 -1
- package/src/server/runtime.ts +4 -1
- package/src/version.ts +1 -1
package/package.json
CHANGED
package/src/compile/index.ts
CHANGED
|
@@ -19,7 +19,7 @@ export {
|
|
|
19
19
|
type ServerExportKind,
|
|
20
20
|
type ServerManifest,
|
|
21
21
|
} from "./serverSplit"
|
|
22
|
-
export { deriveEndpointParams, EndpointTypeError,
|
|
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 (/^
|
|
69
|
+
if (/^async\s+function\b/.test(s)) return "fn"
|
|
64
70
|
if (/^async\b/.test(s)) return "fn" // async (…) => / async x =>
|
|
65
|
-
|
|
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
|
|
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*
|
|
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.
|
|
51
|
-
* not `async` is
|
|
52
|
-
*
|
|
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
|
|
55
|
+
export class ServerHelperImportError extends Error {
|
|
55
56
|
readonly problems: string[]
|
|
56
57
|
constructor(problems: string[]) {
|
|
57
|
-
super(`
|
|
58
|
-
this.name = "
|
|
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))
|
|
@@ -339,6 +339,7 @@ export const defineDb = <const S extends Schema>(declared: S & ValidateRefs<S>,
|
|
|
339
339
|
Object.defineProperties(db, {
|
|
340
340
|
$schema: { get: () => seal().schema },
|
|
341
341
|
$models: { get: () => seal().models },
|
|
342
|
+
$given: { get: () => own },
|
|
342
343
|
$auth: { get: () => seal().auth },
|
|
343
344
|
$files: { get: () => seal().files },
|
|
344
345
|
})
|
package/src/server/db/types.ts
CHANGED
|
@@ -203,7 +203,11 @@ type DbBase<S extends Schema> = {
|
|
|
203
203
|
$transaction<P extends readonly Op<any>[]>(ops: [...P]): Promise<{ [K in keyof P]: P[K] extends Op<infer T> ? T : never }>
|
|
204
204
|
/** The `.marci` schema text this db syncs with. */
|
|
205
205
|
readonly $schema: string
|
|
206
|
-
/** @internal
|
|
206
|
+
/** @internal the models as the schema has them — a model with a `t.file()` field carries the files' relation. */
|
|
207
|
+
readonly $models: Record<string, Model>
|
|
208
|
+
/** @internal the models as `defineDb` received them, by their names here — the objects the project holds,
|
|
209
|
+
* so a host can tell which name the project gave a model it knows by identity. */
|
|
210
|
+
readonly $given: Record<string, Model>
|
|
207
211
|
/** @internal the collections sign-in works on; null = a db without `withAuth`. */
|
|
208
212
|
readonly $auth: AuthBinding | null
|
|
209
213
|
/** @internal the schema's `t.file()` fields (../files/models.ts). */
|
package/src/server/runtime.ts
CHANGED
|
@@ -40,9 +40,12 @@ export const loadServerModules = (
|
|
|
40
40
|
if (!reg) throw new Error("loadServerModules: no server bundle registered (globalThis.__lecodesServer)")
|
|
41
41
|
const out: LoadedServer = { endpoints: new Map(), channels: new Map(), dbs: [], manifest: reg.manifest }
|
|
42
42
|
for (const [mod, exports] of Object.entries(reg.modules)) {
|
|
43
|
+
// the compile's word on each export (serverSplit.ts): a function that is not async is the
|
|
44
|
+
// server's helper, never an endpoint — an old manifest without kinds counts every function
|
|
45
|
+
const kinds = new Map((reg.manifest?.modules?.[mod] ?? []).map(e => [e.name, e.kind]))
|
|
43
46
|
for (const [name, value] of Object.entries(exports)) {
|
|
44
47
|
const id = `${mod}#${name}`
|
|
45
|
-
if (typeof value === "function") out.endpoints.set(id, value as (...args: any[]) => unknown)
|
|
48
|
+
if (typeof value === "function") { if (kinds.get(name) !== "other") out.endpoints.set(id, value as (...args: any[]) => unknown) }
|
|
46
49
|
else if (isChannel(value)) { value.__id = id; out.channels.set(id, value) }
|
|
47
50
|
else if (isDb(value)) out.dbs.push(value)
|
|
48
51
|
}
|
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.
|
|
7
|
+
export const SDK_VERSION = "2.0.13"
|
|
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 => {
|