lecodes-cli 0.18.1 → 0.18.2

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 (33) hide show
  1. package/dist/index.js +1008 -488
  2. package/package.json +1 -1
  3. package/runtime/scene-harness.json +1 -1
  4. package/runtime/sdk/gl/Camera.ts +39 -0
  5. package/runtime/sdk/gl/CameraPlace.ts +52 -52
  6. package/runtime/sdk/gl/Scene.ts +13 -0
  7. package/runtime/sdk/gl/Trigger.ts +45 -45
  8. package/runtime/sdk/gl/scenarios.ts +291 -291
  9. package/runtime/sdk/gl/state.ts +6 -6
  10. package/runtime/sdk/runtime/device.ts +102 -1
  11. package/runtime/sdk/scene/defineScene.ts +1227 -1227
  12. package/runtime/sdk/scene/gizmos.ts +148 -148
  13. package/runtime/sdk-types.json +1 -1
  14. package/runtime/sdk/compile/aspectMacro.ts +0 -86
  15. package/runtime/sdk/compile/assetIconMacro.ts +0 -384
  16. package/runtime/sdk/compile/assetMacro.ts +0 -146
  17. package/runtime/sdk/compile/assetName.ts +0 -50
  18. package/runtime/sdk/compile/bundler.ts +0 -287
  19. package/runtime/sdk/compile/compileProject.ts +0 -129
  20. package/runtime/sdk/compile/detectEntry.ts +0 -128
  21. package/runtime/sdk/compile/fontMacro.ts +0 -459
  22. package/runtime/sdk/compile/fontRegistry.ts +0 -78
  23. package/runtime/sdk/compile/header.ts +0 -67
  24. package/runtime/sdk/compile/index.ts +0 -100
  25. package/runtime/sdk/compile/libraryImports.ts +0 -52
  26. package/runtime/sdk/compile/liteMaterial.ts +0 -247
  27. package/runtime/sdk/compile/sceneEditor.ts +0 -88
  28. package/runtime/sdk/compile/serverSplit.ts +0 -233
  29. package/runtime/sdk/compile/serverTypes.ts +0 -227
  30. package/runtime/sdk/compile/sfnt.ts +0 -98
  31. package/runtime/sdk/compile/shaderSchema.ts +0 -202
  32. package/runtime/sdk/compile/shaderTargets.ts +0 -81
  33. package/runtime/sdk/compile/sourcemap.ts +0 -25
@@ -1,233 +0,0 @@
1
- /**
2
- * The client/server split of a project (docs/backend-plan.md §4).
3
- *
4
- * `*.server.ts` files never reach the client bundle: `compileProject` swaps each one for a STUB
5
- * module whose exports are RPC/channel proxies (`__rpc`, `__channel`, `__serverOnly` — client SDK
6
- * globals, src/runtime/rpc.ts), so `import { listNotes } from "./notes.server"` in client code keeps
7
- * its types and calls the server. `compileServerBundle` bundles the same files with the SERVER inject
8
- * behind a synthetic entry that registers every module on `globalThis.__lecodesServer` (chisel keeps
9
- * side effects, not entry exports) — the runner reads that (src/server/runtime.ts).
10
- *
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.
13
- */
14
-
15
- import type { AppConfig } from "../server/auth/appConfig"
16
- import { bundleProjectWithMap, SERVER_INJECT, type MemoryFiles } from "./bundler"
17
- import type { CompileEntry } from "./compileProject"
18
- import { deriveEndpointParams } from "./serverTypes"
19
-
20
- export const SERVER_FILE = /\.server\.[tj]s$/
21
- export const isServerPath = (path: string) => SERVER_FILE.test(path)
22
-
23
- /** Project path → endpoint-id prefix: no leading `./` or `/`, forward slashes. */
24
- export const serverModuleId = (path: string) => path.split("\\").join("/").replace(/^(\.\/|\/)+/, "")
25
-
26
- export type ServerExportKind = "fn" | "channel" | "other"
27
- export type ServerExport = { name: string, kind: ServerExportKind }
28
-
29
- // ───────────────────────────── static export scan ─────────────────────────────
30
-
31
- /** Blank out comments and string/template contents (keeps line structure) so the regexes below don't
32
- * trip on `export` inside a comment or a string. */
33
- export const blankCommentsAndStrings = (src: string): string => {
34
- let out = ""
35
- let i = 0
36
- const n = src.length
37
- while (i < n) {
38
- const c = src[i], d = src[i + 1]
39
- if (c === "/" && d === "/") { while (i < n && src[i] !== "\n") { out += " "; i++ } continue }
40
- if (c === "/" && d === "*") {
41
- out += " "; i += 2
42
- while (i < n && !(src[i] === "*" && src[i + 1] === "/")) { out += src[i] === "\n" ? "\n" : " "; i++ }
43
- out += " "; i += 2; continue
44
- }
45
- if (c === '"' || c === "'" || c === "`") {
46
- const q = c
47
- out += q; i++
48
- while (i < n && src[i] !== q) {
49
- if (src[i] === "\\") { out += " "; i += 2; continue }
50
- out += src[i] === "\n" ? "\n" : " "; i++
51
- }
52
- out += q; i++; continue
53
- }
54
- out += c; i++
55
- }
56
- return out
57
- }
58
-
59
- const IDENT = "[A-Za-z_$][\\w$]*"
60
- const classifyInit = (init: string): ServerExportKind => {
61
- const s = init.trimStart()
62
- if (/^channel\s*[<(]/.test(s)) return "channel"
63
- if (/^(async\s+)?function\b/.test(s)) return "fn"
64
- 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"
75
- }
76
-
77
- /** Exported names of a server module with their kinds. Default exports are rejected. */
78
- export const scanServerExports = (source: string, path = "<server module>"): ServerExport[] => {
79
- const src = blankCommentsAndStrings(source)
80
- if (/(^|\n)\s*export\s+default\b/.test(src)) {
81
- throw new Error(`${path}: default exports are not endpoints — use named exports`)
82
- }
83
- // (position, name, kind) — kept in source order so ids/manifests are stable and readable
84
- const found: { at: number, name: string, kind: ServerExportKind }[] = []
85
- const add = (at: number, name: string, kind: ServerExportKind) => {
86
- const i = found.findIndex(f => f.name === name)
87
- if (i >= 0) found[i] = { at: found[i].at, name, kind }
88
- else found.push({ at, name, kind })
89
- }
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")
92
- // export const|let|var name [: T] = init
93
- for (const m of src.matchAll(new RegExp(`(^|\\n)\\s*export\\s+(?:const|let|var)\\s+(${IDENT})\\s*(?::[^=]*?)?=\\s*([^\\n]*)`, "g"))) {
94
- add(m.index!, m[2], classifyInit(m[3]))
95
- }
96
- // export class name → server-only
97
- for (const m of src.matchAll(new RegExp(`(^|\\n)\\s*export\\s+class\\s+(${IDENT})`, "g"))) add(m.index!, m[2], "other")
98
- // export { a, b as c } → classify by the local declaration
99
- for (const m of src.matchAll(/(^|\n)\s*export\s*\{([^}]*)\}/g)) {
100
- for (const part of m[2].split(",")) {
101
- const [local, exported] = part.split(/\s+as\s+/).map(s => s.trim())
102
- if (!local || /^type\s/.test(local)) continue
103
- const name = exported ?? local
104
- let kind: ServerExportKind = "other"
105
- if (new RegExp(`(^|\\n)\\s*(async\\s+)?function\\s*\\*?\\s*${local}\\b`).test(src)) kind = "fn"
106
- else {
107
- const decl = src.match(new RegExp(`(^|\\n)\\s*(?:const|let|var)\\s+${local}\\s*(?::[^=]*?)?=\\s*([^\\n]*)`))
108
- if (decl) kind = classifyInit(decl[2])
109
- }
110
- add(m.index!, name, kind)
111
- }
112
- }
113
- return found.sort((a, b) => a.at - b.at).map(({ name, kind }) => ({ name, kind }))
114
- }
115
-
116
- // ───────────────────────────── client stubs ─────────────────────────────
117
-
118
- /** The client-side replacement of a `*.server.ts` module. `serverUrl` is baked in (draft/prod are
119
- * separate compiles); "" means "no server deployed" — calls fail with a clear error. */
120
- export const serverStubSource = (path: string, exports: ServerExport[], serverUrl: string): string => {
121
- const mod = serverModuleId(path)
122
- const url = JSON.stringify(serverUrl)
123
- const lines = exports.map(({ name, kind }) => {
124
- const id = JSON.stringify(`${mod}#${name}`)
125
- if (kind === "fn") return `export const ${name} = __rpc(${url}, ${id})`
126
- if (kind === "channel") return `export const ${name} = __channel(${url}, ${id})`
127
- return `export const ${name} = __serverOnly(${id})`
128
- })
129
- return `// client stub of ${mod} — generated by the lecodes compiler\n${lines.join("\n")}\n`
130
- }
131
-
132
- // ───────────────────────────── server entry ─────────────────────────────
133
-
134
- export const SERVER_ENTRY_PATH = "/.server-entry.ts"
135
- /** 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
137
- * app.json as an ordinary import. */
138
- export const APP_CONFIG_PATH = "/__sdk/server/auth/appConfig.ts"
139
-
140
- /** Compile-time manifest embedded in the server bundle (validators land here — see serverTypes.ts). */
141
- export type ServerManifest = {
142
- modules: Record<string, ServerExport[]>
143
- /** endpoint id → JSON-schema-ish param list (filled by the TS-checker pass; absent = unvalidated). */
144
- params?: Record<string, unknown[]>
145
- /** The `app.json` subset the server runtime reads (name, auth providers); the bundle itself gets it as the
146
- * compiled-in `appConfig` module (APP_CONFIG_PATH). */
147
- 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
- }
152
-
153
- /** The synthetic entry: imports every export by name (chisel has no namespace imports) and registers
154
- * the module map + manifest on `globalThis.__lecodesServer` — a side effect chisel keeps. */
155
- export const serverEntrySource = (modules: Record<string, ServerExport[]>, manifest: ServerManifest): string => {
156
- const imports: string[] = []
157
- const entries: string[] = []
158
- Object.entries(modules).forEach(([path, exports], i) => {
159
- const names = exports.map(e => `${e.name} as m${i}_${e.name}`).join(", ")
160
- if (names) imports.push(`import { ${names} } from ${JSON.stringify(path)}`)
161
- else imports.push(`import ${JSON.stringify(path)}`)
162
- const props = exports.map(e => `${JSON.stringify(e.name)}: m${i}_${e.name}`).join(", ")
163
- entries.push(`${JSON.stringify(serverModuleId(path))}: { ${props} }`)
164
- })
165
- return `${imports.join("\n")}\n;(globalThis as any).__lecodesServer = { modules: { ${entries.join(", ")} }, manifest: ${JSON.stringify(manifest)} }\n`
166
- }
167
-
168
- const normalizePath = (path: string) => {
169
- const parts: string[] = []
170
- for (const part of path.split("\\").join("/").split("/")) {
171
- if (!part || part === ".") continue
172
- if (part === "..") parts.pop()
173
- else parts.push(part)
174
- }
175
- return "/" + parts.join("/")
176
- }
177
-
178
- /** Scan every `*.server.ts` entry → module path → exports. Throws on a default export. */
179
- export const scanServerModules = (entries: CompileEntry[]): Record<string, ServerExport[]> => {
180
- const out: Record<string, ServerExport[]> = {}
181
- for (const e of entries) {
182
- if (e.type !== "text" || !isServerPath(e.path)) continue
183
- out[normalizePath(e.path)] = scanServerExports(e.text ?? "", serverModuleId(e.path))
184
- }
185
- return out
186
- }
187
-
188
- /** Replace each `*.server.ts` entry with its client stub (returns a new entries array). */
189
- export const stubServerEntries = (entries: CompileEntry[], serverUrl: string): CompileEntry[] =>
190
- entries.map(e => e.type === "text" && isServerPath(e.path)
191
- ? { ...e, text: serverStubSource(e.path, scanServerExports(e.text ?? "", serverModuleId(e.path)), serverUrl) }
192
- : e)
193
-
194
- export type CompileServerOptions = {
195
- entries: CompileEntry[]
196
- /** Derive per-endpoint argument validators from the TS parameter types (serverTypes.ts) and embed
197
- * them in the manifest. Default true; throws `EndpointTypeError` on non-JSON parameter types. */
198
- validate?: boolean
199
- /** Precomputed param schemas to embed instead of deriving them. */
200
- params?: Record<string, unknown[]>
201
- /** Server bundles are readable by default (stack traces in the server console). */
202
- minify?: boolean
203
- sourcemap?: boolean
204
- /** The project's `app.json` (name + auth providers) — embedded in the manifest and exposed to the bundle. */
205
- app?: AppConfig
206
- }
207
-
208
- /** Bundle the project's server side. Returns the code (a script that registers
209
- * `globalThis.__lecodesServer`) and the manifest; `null` when the project has no server files. */
210
- export const compileServerBundle = async (opts: CompileServerOptions): Promise<{ code: string, manifest: ServerManifest } | null> => {
211
- const modules = scanServerModules(opts.entries)
212
- if (Object.keys(modules).length === 0) return null
213
- const files: MemoryFiles = {}
214
- for (const e of opts.entries) {
215
- if (e.type !== "text") continue
216
- if (e.path.toLowerCase().endsWith(".svg")) continue
217
- files[normalizePath(e.path)] = e.text ?? ""
218
- }
219
- let params = opts.params
220
- if (!params && opts.validate !== false) {
221
- const derived = await deriveEndpointParams(opts.entries, modules)
222
- for (const w of derived.warnings) console.warn(`warning: ${w}`)
223
- params = derived.params
224
- }
225
- const usesAuth = opts.entries.some(e => e.type === "text" && isServerPath(e.path) && /\bauth\s*\./.test(blankCommentsAndStrings(e.text ?? "")))
226
- const manifest: ServerManifest = { modules: Object.fromEntries(Object.entries(modules).map(([p, ex]) => [serverModuleId(p), ex])), params, app: opts.app, usesAuth }
227
- if (opts.app) files[APP_CONFIG_PATH] = `export const app = ${JSON.stringify(opts.app)}\n`
228
- files[SERVER_ENTRY_PATH] = serverEntrySource(modules, manifest)
229
- const { code } = await bundleProjectWithMap(
230
- SERVER_ENTRY_PATH, files, "esm", opts.minify ?? false, false, undefined, opts.sourcemap ?? false, SERVER_INJECT,
231
- )
232
- return { code, manifest }
233
- }
@@ -1,227 +0,0 @@
1
- /**
2
- * Endpoint parameter schemas from TypeScript types (docs/backend-plan.md §2.3 / §4.3).
3
- *
4
- * Endpoints are plain functions; their argument types are ordinary TS annotations. The client is not
5
- * trusted, so the server validates every call — and rather than making developers write a schema
6
- * twice, this pass runs the TypeScript checker over the project's `*.server.ts` files (in memory) and
7
- * lowers each exported function's parameter types to the compact JSON schema `src/server/validate.ts`
8
- * interprets at runtime. The result is embedded in the server bundle's manifest.
9
- *
10
- * Supported: primitives, literals, unions, enums, arrays, tuples, object types / interfaces / type
11
- * aliases (from any project file), optional properties, index signatures (`Record<string, T>`),
12
- * optional / defaulted / rest parameters, `any`/`unknown` (= accept anything). Rejected with a
13
- * compile error: functions, classes with methods, `Date`, `Map`/`Set`, symbols, recursive types —
14
- * "endpoint args must be JSON-serializable". Return types are not validated.
15
- *
16
- * `typescript` is loaded lazily (it's a devDependency of the SDK and a dependency of the platform
17
- * compile step); the browser-safe parts of the compiler never import this module's runtime.
18
- */
19
-
20
- import type { CompileEntry } from "./compileProject"
21
- import type { ParamSchema, Schema } from "../server/validate"
22
- import { serverModuleId, type ServerExport } from "./serverSplit"
23
-
24
- type TS = typeof import("typescript")
25
-
26
- const normalizePath = (path: string) => {
27
- const parts: string[] = []
28
- for (const part of path.split("\\").join("/").split("/")) {
29
- if (!part || part === ".") continue
30
- if (part === "..") parts.pop()
31
- else parts.push(part)
32
- }
33
- return "/" + parts.join("/")
34
- }
35
-
36
- export class EndpointTypeError extends Error {
37
- readonly problems: string[]
38
- constructor(problems: string[]) {
39
- super(`endpoint args must be JSON-serializable:\n ${problems.join("\n ")}`)
40
- this.name = "EndpointTypeError"
41
- this.problems = problems
42
- }
43
- }
44
-
45
- export type DeriveResult = {
46
- /** endpoint id → parameter schemas (only endpoints the checker could see). */
47
- params: Record<string, ParamSchema[]>
48
- warnings: string[]
49
- }
50
-
51
- /**
52
- * Derive `{ "<module>#<fn>": ParamSchema[] }` for every function export of every `*.server.ts` entry.
53
- * Throws `EndpointTypeError` when a parameter type can't be validated at runtime.
54
- */
55
- export const deriveEndpointParams = async (entries: CompileEntry[], modules: Record<string, ServerExport[]>): Promise<DeriveResult> => {
56
- let ts: TS
57
- try {
58
- ts = (await import("typescript")).default ?? (await import("typescript")) as unknown as TS
59
- } catch {
60
- throw new Error("serverTypes: the `typescript` package is required to compile server endpoints")
61
- }
62
-
63
- const files = new Map<string, string>()
64
- for (const e of entries) {
65
- if (e.type !== "text" || typeof e.text !== "string") continue
66
- if (!/\.[tj]sx?$/.test(e.path)) continue
67
- files.set(normalizePath(e.path), e.text)
68
- }
69
- const roots = Object.keys(modules).map(normalizePath).filter(p => files.has(p))
70
-
71
- const options: import("typescript").CompilerOptions = {
72
- target: ts.ScriptTarget.ESNext,
73
- module: ts.ModuleKind.ESNext,
74
- moduleResolution: ts.ModuleResolutionKind.Bundler,
75
- allowImportingTsExtensions: true,
76
- strict: true,
77
- noEmit: true,
78
- skipLibCheck: true,
79
- types: [],
80
- lib: ["lib.es2022.d.ts"],
81
- }
82
- const defaultLibPath = ts.getDefaultLibFilePath(options).split("\\").join("/")
83
- const defaultLibDir = defaultLibPath.slice(0, defaultLibPath.lastIndexOf("/"))
84
- const sys = ts.sys
85
- const host: import("typescript").CompilerHost = {
86
- fileExists: p => files.has(normalizePath(p)) || (p.split("\\").join("/").startsWith(defaultLibDir) && sys.fileExists(p)),
87
- readFile: p => files.get(normalizePath(p)) ?? (p.split("\\").join("/").startsWith(defaultLibDir) ? sys.readFile(p) : undefined),
88
- getSourceFile: (p, lang) => {
89
- const text = files.get(normalizePath(p)) ?? (p.split("\\").join("/").startsWith(defaultLibDir) ? sys.readFile(p) : undefined)
90
- return text === undefined ? undefined : ts.createSourceFile(p, text, lang, true)
91
- },
92
- getDefaultLibFileName: o => ts.getDefaultLibFilePath(o),
93
- writeFile: () => {},
94
- getCurrentDirectory: () => "/",
95
- getCanonicalFileName: p => p,
96
- useCaseSensitiveFileNames: () => true,
97
- getNewLine: () => "\n",
98
- directoryExists: () => true,
99
- getDirectories: () => [],
100
- }
101
- const program = ts.createProgram(roots, options, host)
102
- const checker = program.getTypeChecker()
103
-
104
- const params: Record<string, ParamSchema[]> = {}
105
- const warnings: string[] = []
106
- const problems: string[] = []
107
-
108
- for (const [path, exports] of Object.entries(modules)) {
109
- const sf = program.getSourceFile(normalizePath(path))
110
- if (!sf) { warnings.push(`${path}: not in the TS program`); continue }
111
- const modSym = checker.getSymbolAtLocation(sf)
112
- if (!modSym) continue
113
- const exported = new Map(checker.getExportsOfModule(modSym).map(s => [s.name, s]))
114
- for (const ex of exports) {
115
- if (ex.kind !== "fn") continue
116
- const id = `${serverModuleId(path)}#${ex.name}`
117
- let sym = exported.get(ex.name)
118
- if (!sym) { warnings.push(`${id}: export not found by the checker — unvalidated`); continue }
119
- if (sym.flags & ts.SymbolFlags.Alias) sym = checker.getAliasedSymbol(sym)
120
- const decl = sym.valueDeclaration ?? sym.declarations?.[0]
121
- if (!decl) { warnings.push(`${id}: no declaration — unvalidated`); continue }
122
- const type = checker.getTypeOfSymbolAtLocation(sym, decl)
123
- const sig = type.getCallSignatures()[0]
124
- if (!sig) { warnings.push(`${id}: not callable — unvalidated`); continue }
125
- const list: ParamSchema[] = []
126
- for (const p of sig.getParameters()) {
127
- const pd = p.valueDeclaration as import("typescript").ParameterDeclaration | undefined
128
- const ptype = checker.getTypeOfSymbolAtLocation(p, pd ?? decl)
129
- const rest = !!pd?.dotDotDotToken
130
- const optional = !!pd?.questionToken || !!pd?.initializer || rest
131
- try {
132
- // `x?: T` carries `undefined` in its type — the optional flag covers it, strip it from the schema
133
- const members = ptype.flags & ts.TypeFlags.Union ? (ptype as import("typescript").UnionType).types : [ptype]
134
- const schema = lowerMembers(ts, checker, pd?.questionToken ? members.filter(t => !(t.flags & ts.TypeFlags.Undefined)) : members, `${id}(${p.name})`, new Set())
135
- list.push({ name: p.name, schema, optional, rest })
136
- } catch (e) {
137
- problems.push((e as Error).message)
138
- }
139
- }
140
- params[id] = list
141
- }
142
- }
143
- if (problems.length) throw new EndpointTypeError(problems)
144
- return { params, warnings }
145
- }
146
-
147
- // ───────────────────────────── type → schema ─────────────────────────────
148
-
149
- const lower = (ts: TS, checker: import("typescript").TypeChecker, type: import("typescript").Type, where: string, stack: Set<import("typescript").Type>): Schema => {
150
- const f = type.flags
151
- const T = ts.TypeFlags
152
- if (f & (T.Any | T.Unknown)) return { t: "any" }
153
- if (f & T.Never) throw new Error(`${where}: never`)
154
- if (f & T.Null) return { t: "null" }
155
- if (f & (T.Undefined | T.Void)) return { t: "undefined" }
156
- if (f & T.BooleanLiteral) return { t: "lit", v: (type as any).intrinsicName === "true" }
157
- if (f & T.Boolean) return { t: "boolean" }
158
- if (f & T.StringLiteral) return { t: "lit", v: (type as import("typescript").StringLiteralType).value }
159
- if (f & T.NumberLiteral) return { t: "lit", v: (type as import("typescript").NumberLiteralType).value }
160
- if (f & T.String) return { t: "string" }
161
- if (f & T.Number) return { t: "number" }
162
- if (f & (T.BigInt | T.BigIntLiteral | T.ESSymbol | T.UniqueESSymbol)) throw new Error(`${where}: ${checker.typeToString(type)} is not JSON`)
163
- if (f & T.EnumLiteral && f & T.Union) {
164
- return { t: "union", of: (type as import("typescript").UnionType).types.map(t => lower(ts, checker, t, where, stack)) }
165
- }
166
- if (f & T.Union) return lowerMembers(ts, checker, (type as import("typescript").UnionType).types, where, stack)
167
- if (f & T.Intersection || f & T.Object) {
168
- if (stack.has(type)) throw new Error(`${where}: recursive type ${checker.typeToString(type)}`)
169
- if (type.getCallSignatures().length || type.getConstructSignatures().length) throw new Error(`${where}: functions are not JSON`)
170
- stack.add(type)
171
- try {
172
- if (checker.isArrayType(type)) {
173
- const [el] = checker.getTypeArguments(type as import("typescript").TypeReference)
174
- return { t: "array", of: el ? lower(ts, checker, el, where, stack) : { t: "any" } }
175
- }
176
- if (checker.isTupleType(type)) {
177
- const ref = type as import("typescript").TypeReference
178
- const target = ref.target as import("typescript").TupleType
179
- const args = checker.getTypeArguments(ref)
180
- const items: Schema[] = []
181
- let rest: Schema | undefined
182
- target.elementFlags.forEach((flag, i) => {
183
- const s = lower(ts, checker, args[i], where, stack)
184
- if (flag & ts.ElementFlags.Rest || flag & ts.ElementFlags.Variadic) rest = (s.t === "array" ? s.of : s)
185
- else items.push(flag & ts.ElementFlags.Optional ? { t: "union", of: [s, { t: "undefined" }] } : s)
186
- })
187
- return { t: "tuple", items, rest }
188
- }
189
- const name = checker.typeToString(type)
190
- if (/^(Date|Map<|Set<|WeakMap<|WeakSet<|Promise<|RegExp$|ArrayBuffer$|Uint8Array$)/.test(name)) throw new Error(`${where}: ${name} is not JSON`)
191
- const props: Record<string, Schema> = {}
192
- const optional: string[] = []
193
- for (const p of checker.getPropertiesOfType(type)) {
194
- const pt = checker.getTypeOfSymbolAtLocation(p, p.valueDeclaration ?? p.declarations?.[0]!)
195
- if (pt.getCallSignatures().length) throw new Error(`${where}.${p.name}: functions are not JSON`)
196
- const isOpt = !!(p.flags & ts.SymbolFlags.Optional)
197
- // an optional property's type carries `undefined` — strip it, the optional list covers it
198
- const members = pt.flags & T.Union ? (pt as import("typescript").UnionType).types : [pt]
199
- props[p.name] = lowerMembers(ts, checker, isOpt ? members.filter(t => !(t.flags & T.Undefined)) : members, `${where}.${p.name}`, stack)
200
- if (isOpt) optional.push(p.name)
201
- }
202
- let index: Schema | undefined
203
- for (const info of checker.getIndexInfosOfType(type)) {
204
- if (info.keyType.flags & T.String) index = lower(ts, checker, info.type, `${where}[string]`, stack)
205
- }
206
- return { t: "object", props, ...(optional.length ? { optional } : {}), ...(index ? { index } : {}) }
207
- } finally {
208
- stack.delete(type)
209
- }
210
- }
211
- if (f & T.TypeParameter) throw new Error(`${where}: generic parameter ${checker.typeToString(type)} can't be validated`)
212
- throw new Error(`${where}: unsupported type ${checker.typeToString(type)}`)
213
- }
214
-
215
- /** Lower a list of union members: `boolean` arrives as `true | false` — fold it back; a single member is itself. */
216
- const lowerMembers = (ts: TS, checker: import("typescript").TypeChecker, members: readonly import("typescript").Type[], where: string, stack: Set<import("typescript").Type>): Schema => {
217
- const T = ts.TypeFlags
218
- if (members.length === 1) return lower(ts, checker, members[0], where, stack)
219
- const isLit = (m: import("typescript").Type, v: string) => !!(m.flags & T.BooleanLiteral) && (m as any).intrinsicName === v
220
- const foldBool = members.some(m => isLit(m, "true")) && members.some(m => isLit(m, "false"))
221
- const of: Schema[] = foldBool ? [{ t: "boolean" }] : []
222
- for (const m of members) {
223
- if (foldBool && m.flags & T.BooleanLiteral) continue
224
- of.push(lower(ts, checker, m, where, stack))
225
- }
226
- return of.length === 1 ? of[0] : { t: "union", of }
227
- }
@@ -1,98 +0,0 @@
1
- // Minimal sfnt (TTF/OTF) reader for the `font()` compile macro: family name from the `name` table,
2
- // weight/italic from `OS/2` (with a `head.macStyle` fallback). Reads only what the macro needs —
3
- // this is not a font sanitizer or validator.
4
-
5
- export type SfntInfo = { family: string, weight: number, italic: boolean }
6
-
7
- const TAG_TTF = 0x00010000
8
- const TAG_OTTO = 0x4f54544f // 'OTTO' (CFF outlines)
9
- const TAG_TTCF = 0x74746366 // 'ttcf' (collection)
10
- const TAG_WOFF = 0x774f4646 // 'wOFF'
11
- const TAG_WOF2 = 0x774f4632 // 'wOF2'
12
-
13
- /** Parse a .ttf/.otf. Throws a message meant to surface verbatim in a compile error. */
14
- export const parseSfnt = (bytes: Uint8Array): SfntInfo => {
15
- const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
16
- if (bytes.byteLength < 12) throw new Error("not a font file (truncated)")
17
- const version = dv.getUint32(0)
18
- if (version === TAG_TTCF) throw new Error("font collections (.ttc) aren't supported — export a single face")
19
- if (version === TAG_WOFF || version === TAG_WOF2) throw new Error("WOFF/WOFF2 project fonts aren't supported — use the .ttf/.otf")
20
- if (version !== TAG_TTF && version !== TAG_OTTO) throw new Error("not a TTF/OTF font file")
21
-
22
- const numTables = dv.getUint16(4)
23
- const tables = new Map<string, { offset: number, length: number }>()
24
- for (let i = 0; i < numTables; i++) {
25
- const rec = 12 + i * 16
26
- if (rec + 16 > bytes.byteLength) throw new Error("not a font file (truncated table directory)")
27
- const tag = String.fromCharCode(bytes[rec], bytes[rec + 1], bytes[rec + 2], bytes[rec + 3])
28
- tables.set(tag, { offset: dv.getUint32(rec + 8), length: dv.getUint32(rec + 12) })
29
- }
30
-
31
- const family = readFamily(dv, bytes, tables.get("name"))
32
- if (!family) throw new Error("no family name in the font's `name` table")
33
-
34
- let weight = 400
35
- let italic = false
36
- const os2 = tables.get("OS/2")
37
- if (os2 && os2.offset + 64 <= bytes.byteLength) {
38
- weight = dv.getUint16(os2.offset + 4) // usWeightClass
39
- italic = (dv.getUint16(os2.offset + 62) & 1) === 1 // fsSelection ITALIC
40
- } else {
41
- const head = tables.get("head")
42
- if (head && head.offset + 46 <= bytes.byteLength) {
43
- const macStyle = dv.getUint16(head.offset + 44)
44
- if (macStyle & 1) weight = 700
45
- italic = (macStyle & 2) === 2
46
- }
47
- }
48
- if (weight < 1 || weight > 1000) weight = 400
49
-
50
- return { family, weight, italic }
51
- }
52
-
53
- /** Best family name: typographic family (nameID 16) over legacy family (1); Windows-English
54
- * records over the rest. Returns "" when the table yields nothing decodable. */
55
- const readFamily = (dv: DataView, bytes: Uint8Array, name?: { offset: number, length: number }): string => {
56
- if (!name || name.offset + 6 > bytes.byteLength) return ""
57
- const base = name.offset
58
- const count = dv.getUint16(base + 2)
59
- const stringOffset = base + dv.getUint16(base + 4)
60
-
61
- let best = ""
62
- let bestScore = -1
63
- for (let i = 0; i < count; i++) {
64
- const rec = base + 6 + i * 12
65
- if (rec + 12 > bytes.byteLength) break
66
- const platformID = dv.getUint16(rec)
67
- const languageID = dv.getUint16(rec + 4)
68
- const nameID = dv.getUint16(rec + 6)
69
- const length = dv.getUint16(rec + 8)
70
- const offset = stringOffset + dv.getUint16(rec + 10)
71
- if (nameID !== 16 && nameID !== 1) continue
72
- if (offset + length > bytes.byteLength) continue
73
-
74
- // nameID 16 beats 1; within an id, Windows-English > Windows > Unicode > Mac.
75
- let score = nameID === 16 ? 100 : 0
76
- if (platformID === 3) score += languageID === 0x409 ? 30 : 20
77
- else if (platformID === 0) score += 10
78
- if (score <= bestScore) continue
79
-
80
- const text = platformID === 1 ? decodeLatin1(bytes, offset, length) : decodeUtf16be(bytes, offset, length)
81
- if (!text.trim()) continue
82
- best = text.trim()
83
- bestScore = score
84
- }
85
- return best
86
- }
87
-
88
- const decodeUtf16be = (bytes: Uint8Array, offset: number, length: number): string => {
89
- let out = ""
90
- for (let i = 0; i + 1 < length; i += 2) out += String.fromCharCode((bytes[offset + i] << 8) | bytes[offset + i + 1])
91
- return out
92
- }
93
-
94
- const decodeLatin1 = (bytes: Uint8Array, offset: number, length: number): string => {
95
- let out = ""
96
- for (let i = 0; i < length; i++) out += String.fromCharCode(bytes[offset + i])
97
- return out
98
- }