lecodes-sdk 0.19.0
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 +114 -0
- package/dist/global.d.ts +503 -0
- package/dist/host.d.ts +61 -0
- package/dist/inject.js +4730 -0
- package/dist/types/animate/animate.d.ts +20 -0
- package/dist/types/animate/bezier.d.ts +2 -0
- package/dist/types/animate/easings.d.ts +21 -0
- package/dist/types/canvas/Canvas.d.ts +168 -0
- package/dist/types/core/Aspect.d.ts +171 -0
- package/dist/types/core/InspectorUI.d.ts +88 -0
- package/dist/types/core/StateMachine.d.ts +81 -0
- package/dist/types/core/color.d.ts +13 -0
- package/dist/types/core/compWrite.d.ts +11 -0
- package/dist/types/core/events.d.ts +11 -0
- package/dist/types/core/fields.d.ts +58 -0
- package/dist/types/core/registry.d.ts +7 -0
- package/dist/types/core/signals.d.ts +29 -0
- package/dist/types/core/time.d.ts +31 -0
- package/dist/types/g2/Camera2D.d.ts +19 -0
- package/dist/types/g2/CharacterController2D.d.ts +134 -0
- package/dist/types/g2/Node2D.d.ts +91 -0
- package/dist/types/g2/OneWay2D.d.ts +24 -0
- package/dist/types/g2/Physics2D.d.ts +147 -0
- package/dist/types/g2/Scene2D.d.ts +72 -0
- package/dist/types/g2/Shape2D.d.ts +58 -0
- package/dist/types/g2/Sprite.d.ts +51 -0
- package/dist/types/g2/SpriteAnimation.d.ts +50 -0
- package/dist/types/g2/SpriteSheet.d.ts +69 -0
- package/dist/types/g2/Texture2D.d.ts +16 -0
- package/dist/types/g2/Tilemap.d.ts +20 -0
- package/dist/types/g2/Tileset.d.ts +32 -0
- package/dist/types/g2/Trigger2D.d.ts +20 -0
- package/dist/types/g2/autotile.d.ts +133 -0
- package/dist/types/g2/cells.d.ts +14 -0
- package/dist/types/g2/defineScene2d.d.ts +141 -0
- package/dist/types/g2/groups2d.d.ts +16 -0
- package/dist/types/g2/loop.d.ts +4 -0
- package/dist/types/g2/scenarios2d.d.ts +30 -0
- package/dist/types/g2/touch.d.ts +2 -0
- package/dist/types/gl/Camera.d.ts +79 -0
- package/dist/types/gl/CameraPlace.d.ts +26 -0
- package/dist/types/gl/CharacterController.d.ts +98 -0
- package/dist/types/gl/Gearbox.d.ts +86 -0
- package/dist/types/gl/Geometry.d.ts +39 -0
- package/dist/types/gl/IK.d.ts +53 -0
- package/dist/types/gl/InstancedMesh.d.ts +46 -0
- package/dist/types/gl/Light.d.ts +73 -0
- package/dist/types/gl/Lightmap.d.ts +85 -0
- package/dist/types/gl/Locomotion.d.ts +279 -0
- package/dist/types/gl/Material.d.ts +100 -0
- package/dist/types/gl/Mesh.d.ts +33 -0
- package/dist/types/gl/Model.d.ts +53 -0
- package/dist/types/gl/NavAgent.d.ts +99 -0
- package/dist/types/gl/NavMesh.d.ts +169 -0
- package/dist/types/gl/Node.d.ts +99 -0
- package/dist/types/gl/Noise.d.ts +20 -0
- package/dist/types/gl/Particles.d.ts +277 -0
- package/dist/types/gl/Physics.d.ts +91 -0
- package/dist/types/gl/Plane.d.ts +19 -0
- package/dist/types/gl/Ragdoll.d.ts +86 -0
- package/dist/types/gl/Ray.d.ts +8 -0
- package/dist/types/gl/Scene.d.ts +221 -0
- package/dist/types/gl/Shape.d.ts +122 -0
- package/dist/types/gl/Terrain.d.ts +280 -0
- package/dist/types/gl/Texture.d.ts +27 -0
- package/dist/types/gl/Trigger.d.ts +10 -0
- package/dist/types/gl/Vehicle.d.ts +191 -0
- package/dist/types/gl/Wheel.d.ts +95 -0
- package/dist/types/gl/animation/AnimationClip.d.ts +60 -0
- package/dist/types/gl/animation/Animator.d.ts +219 -0
- package/dist/types/gl/animation/Layer.d.ts +31 -0
- package/dist/types/gl/animation/Loop.d.ts +17 -0
- package/dist/types/gl/animation/Playback.d.ts +36 -0
- package/dist/types/gl/animation/core.d.ts +367 -0
- package/dist/types/gl/controls.d.ts +19 -0
- package/dist/types/gl/physicsEvents.d.ts +1 -0
- package/dist/types/gl/scenarios.d.ts +98 -0
- package/dist/types/gl/state.d.ts +5 -0
- package/dist/types/gl/terrainMesh.d.ts +46 -0
- package/dist/types/gl/touch.d.ts +2 -0
- package/dist/types/inject.d.ts +123 -0
- package/dist/types/math/Mathf.d.ts +39 -0
- package/dist/types/math/mat4.d.ts +66 -0
- package/dist/types/math/quat.d.ts +53 -0
- package/dist/types/math/vec.d.ts +129 -0
- package/dist/types/net/codec.d.ts +25 -0
- package/dist/types/net/core.d.ts +106 -0
- package/dist/types/net/index.d.ts +80 -0
- package/dist/types/net/replication.d.ts +101 -0
- package/dist/types/plugins/camera.d.ts +25 -0
- package/dist/types/plugins/geolocation.d.ts +39 -0
- package/dist/types/plugins/oauth.d.ts +25 -0
- package/dist/types/plugins/permission.d.ts +1 -0
- package/dist/types/plugins/push.d.ts +46 -0
- package/dist/types/plugins/qr.d.ts +21 -0
- package/dist/types/plugins/service.d.ts +33 -0
- package/dist/types/runtime/app.d.ts +67 -0
- package/dist/types/runtime/appEvents.d.ts +7 -0
- package/dist/types/runtime/channel.d.ts +1 -0
- package/dist/types/runtime/clipboard.d.ts +7 -0
- package/dist/types/runtime/datetime.d.ts +80 -0
- package/dist/types/runtime/device.d.ts +126 -0
- package/dist/types/runtime/fetch.d.ts +31 -0
- package/dist/types/runtime/files.d.ts +33 -0
- package/dist/types/runtime/input.d.ts +121 -0
- package/dist/types/runtime/media.d.ts +46 -0
- package/dist/types/runtime/misc.d.ts +11 -0
- package/dist/types/runtime/net.d.ts +15 -0
- package/dist/types/runtime/rpc.d.ts +44 -0
- package/dist/types/runtime/service.d.ts +1 -0
- package/dist/types/runtime/share.d.ts +2 -0
- package/dist/types/runtime/storage.d.ts +5 -0
- package/dist/types/runtime/touch.d.ts +56 -0
- package/dist/types/scene/defineScene.d.ts +296 -0
- package/dist/types/scene/editorPlugins.d.ts +82 -0
- package/dist/types/scene/gizmos.d.ts +56 -0
- package/dist/types/scene/grammar.d.ts +56 -0
- package/dist/types/scene/level.d.ts +98 -0
- package/dist/types/scene/material.d.ts +76 -0
- package/dist/types/server/auth/appConfig.d.ts +16 -0
- package/dist/types/server/auth/global.d.ts +56 -0
- package/dist/types/server/auth/models.d.ts +164 -0
- package/dist/types/server/auth/types.d.ts +56 -0
- package/dist/types/server/channel.d.ts +28 -0
- package/dist/types/server/context.d.ts +27 -0
- package/dist/types/server/db/defineDb.d.ts +36 -0
- package/dist/types/server/db/fields.d.ts +146 -0
- package/dist/types/server/db/index.d.ts +6 -0
- package/dist/types/server/db/marci/query.d.ts +326 -0
- package/dist/types/server/db/types.d.ts +163 -0
- package/dist/types/server/errors.d.ts +8 -0
- package/dist/types/server/inject.d.ts +5 -0
- package/dist/types/ui/NativeView.d.ts +38 -0
- package/dist/types/ui/UI.d.ts +27 -0
- package/dist/types/ui/UIBottomSheet.d.ts +71 -0
- package/dist/types/ui/UIButton.d.ts +30 -0
- package/dist/types/ui/UIContainer.d.ts +33 -0
- package/dist/types/ui/UIImage.d.ts +35 -0
- package/dist/types/ui/UIInput.d.ts +110 -0
- package/dist/types/ui/UIModal.d.ts +70 -0
- package/dist/types/ui/UINode.d.ts +363 -0
- package/dist/types/ui/UIPager.d.ts +123 -0
- package/dist/types/ui/UIPopover.d.ts +39 -0
- package/dist/types/ui/UIScreen.d.ts +57 -0
- package/dist/types/ui/UIScrollable.d.ts +40 -0
- package/dist/types/ui/UISpacer.d.ts +9 -0
- package/dist/types/ui/UITabs.d.ts +96 -0
- package/dist/types/ui/UIText.d.ts +19 -0
- package/dist/types/ui/UIVideo.d.ts +28 -0
- package/dist/types/ui/UIVirtualizedList.d.ts +97 -0
- package/dist/types/ui/UIWidget.d.ts +65 -0
- package/dist/types/ui/fonts.d.ts +9 -0
- package/dist/types/ui/presentable.d.ts +68 -0
- package/dist/types/ui/router.d.ts +40 -0
- package/dist/types/ui/theme.d.ts +26 -0
- package/dist/types.json +1 -0
- package/package.json +46 -0
- package/prompts/2d.md +395 -0
- package/prompts/3d-scene-files.md +113 -0
- package/prompts/3d-scene.md +119 -0
- package/prompts/3d.md +303 -0
- package/prompts/README.md +142 -0
- package/prompts/ar.md +109 -0
- package/prompts/canvas.md +27 -0
- package/prompts/compose.ts +46 -0
- package/prompts/concept.md +49 -0
- package/prompts/core-design.md +75 -0
- package/prompts/core.md +264 -0
- package/prompts/design.md +218 -0
- package/prompts/dist/2d-game.md +1530 -0
- package/prompts/dist/3d-app.md +1672 -0
- package/prompts/dist/ar-app.md +1548 -0
- package/prompts/dist/concept.md +49 -0
- package/prompts/dist/design.md +498 -0
- package/prompts/dist/ui-app.md +1134 -0
- package/prompts/index.md +37 -0
- package/prompts/intake-prompt.md +33 -0
- package/prompts/namer-prompt.md +12 -0
- package/prompts/router-prompt.md +50 -0
- package/prompts/select.ts +111 -0
- package/prompts/ui-design.md +203 -0
- package/prompts/ui.md +803 -0
- package/src/animate/animate.ts +238 -0
- package/src/animate/bezier.ts +138 -0
- package/src/animate/easings.ts +126 -0
- package/src/bridges.d.ts +1345 -0
- package/src/canvas/Canvas.ts +305 -0
- package/src/compile/__tests__/assetIconMacro.test.ts +219 -0
- package/src/compile/__tests__/assetMacro.test.ts +74 -0
- package/src/compile/__tests__/assetName.test.ts +55 -0
- package/src/compile/__tests__/compile.test.ts +287 -0
- package/src/compile/__tests__/detectEntry.test.ts +132 -0
- package/src/compile/__tests__/fontMacro.test.ts +199 -0
- package/src/compile/aspectMacro.ts +86 -0
- package/src/compile/assetIconMacro.ts +384 -0
- package/src/compile/assetMacro.ts +146 -0
- package/src/compile/assetName.ts +50 -0
- package/src/compile/bundler.ts +287 -0
- package/src/compile/compileProject.ts +127 -0
- package/src/compile/detectEntry.ts +128 -0
- package/src/compile/fontMacro.ts +459 -0
- package/src/compile/fontRegistry.ts +78 -0
- package/src/compile/header.ts +67 -0
- package/src/compile/index.ts +100 -0
- package/src/compile/libraryImports.ts +52 -0
- package/src/compile/liteMaterial.ts +247 -0
- package/src/compile/sceneEditor.ts +88 -0
- package/src/compile/serverSplit.ts +233 -0
- package/src/compile/serverTypes.ts +227 -0
- package/src/compile/sfnt.ts +98 -0
- package/src/compile/shaderSchema.ts +202 -0
- package/src/compile/shaderTargets.ts +77 -0
- package/src/compile/sourcemap.ts +25 -0
- package/src/core/Aspect.ts +568 -0
- package/src/core/InspectorUI.ts +212 -0
- package/src/core/StateMachine.ts +308 -0
- package/src/core/__tests__/stateMachine.test.ts +132 -0
- package/src/core/color.ts +66 -0
- package/src/core/compWrite.ts +42 -0
- package/src/core/events.ts +38 -0
- package/src/core/fields.ts +120 -0
- package/src/core/registry.ts +23 -0
- package/src/core/signals.ts +277 -0
- package/src/core/time.ts +84 -0
- package/src/g2/Camera2D.ts +40 -0
- package/src/g2/CharacterController2D.ts +276 -0
- package/src/g2/Node2D.ts +267 -0
- package/src/g2/OneWay2D.ts +66 -0
- package/src/g2/Physics2D.ts +346 -0
- package/src/g2/Scene2D.ts +209 -0
- package/src/g2/Shape2D.ts +259 -0
- package/src/g2/Sprite.ts +89 -0
- package/src/g2/SpriteAnimation.ts +171 -0
- package/src/g2/SpriteSheet.ts +166 -0
- package/src/g2/Texture2D.ts +47 -0
- package/src/g2/Tilemap.ts +41 -0
- package/src/g2/Tileset.ts +71 -0
- package/src/g2/Trigger2D.ts +77 -0
- package/src/g2/autotile.ts +433 -0
- package/src/g2/cells.ts +91 -0
- package/src/g2/defineScene2d.ts +381 -0
- package/src/g2/groups2d.ts +106 -0
- package/src/g2/loop.ts +50 -0
- package/src/g2/scenarios2d.ts +69 -0
- package/src/g2/touch.ts +83 -0
- package/src/gl/Camera.ts +160 -0
- package/src/gl/CameraPlace.ts +52 -0
- package/src/gl/CharacterController.ts +238 -0
- package/src/gl/Gearbox.ts +212 -0
- package/src/gl/Geometry.ts +279 -0
- package/src/gl/IK.ts +193 -0
- package/src/gl/InstancedMesh.ts +132 -0
- package/src/gl/Light.ts +135 -0
- package/src/gl/Lightmap.ts +402 -0
- package/src/gl/Locomotion.ts +498 -0
- package/src/gl/Material.ts +279 -0
- package/src/gl/Mesh.ts +83 -0
- package/src/gl/Model.ts +124 -0
- package/src/gl/NavAgent.ts +337 -0
- package/src/gl/NavMesh.ts +397 -0
- package/src/gl/Node.ts +350 -0
- package/src/gl/Noise.ts +30 -0
- package/src/gl/Particles.ts +676 -0
- package/src/gl/Physics.ts +222 -0
- package/src/gl/Plane.ts +53 -0
- package/src/gl/Ragdoll.ts +270 -0
- package/src/gl/Ray.ts +16 -0
- package/src/gl/Scene.ts +505 -0
- package/src/gl/Shape.ts +409 -0
- package/src/gl/Terrain.ts +1092 -0
- package/src/gl/Texture.ts +63 -0
- package/src/gl/Trigger.ts +45 -0
- package/src/gl/Vehicle.ts +473 -0
- package/src/gl/Wheel.ts +240 -0
- package/src/gl/animation/AnimationClip.ts +204 -0
- package/src/gl/animation/Animator.ts +329 -0
- package/src/gl/animation/Layer.ts +39 -0
- package/src/gl/animation/Loop.ts +31 -0
- package/src/gl/animation/Playback.ts +52 -0
- package/src/gl/animation/core.ts +670 -0
- package/src/gl/controls.ts +95 -0
- package/src/gl/physicsEvents.ts +20 -0
- package/src/gl/scenarios.ts +291 -0
- package/src/gl/state.ts +6 -0
- package/src/gl/terrainMesh.ts +219 -0
- package/src/gl/touch.ts +68 -0
- package/src/host.d.ts +61 -0
- package/src/inject.ts +203 -0
- package/src/math/Mathf.ts +118 -0
- package/src/math/mat4.ts +278 -0
- package/src/math/quat.ts +232 -0
- package/src/math/vec.ts +255 -0
- package/src/net/codec.ts +119 -0
- package/src/net/core.ts +384 -0
- package/src/net/index.ts +181 -0
- package/src/net/replication.ts +622 -0
- package/src/plugins/camera.ts +81 -0
- package/src/plugins/geolocation.ts +123 -0
- package/src/plugins/oauth.ts +61 -0
- package/src/plugins/permission.ts +7 -0
- package/src/plugins/push.ts +132 -0
- package/src/plugins/qr.ts +73 -0
- package/src/plugins/service.ts +47 -0
- package/src/runtime/app.ts +101 -0
- package/src/runtime/appEvents.ts +54 -0
- package/src/runtime/channel.ts +61 -0
- package/src/runtime/clipboard.ts +20 -0
- package/src/runtime/datetime.ts +329 -0
- package/src/runtime/device.ts +293 -0
- package/src/runtime/fetch.ts +77 -0
- package/src/runtime/files.ts +108 -0
- package/src/runtime/input.ts +175 -0
- package/src/runtime/media.ts +111 -0
- package/src/runtime/misc.ts +16 -0
- package/src/runtime/net.ts +36 -0
- package/src/runtime/rpc.ts +218 -0
- package/src/runtime/service.ts +83 -0
- package/src/runtime/share.ts +9 -0
- package/src/runtime/storage.ts +13 -0
- package/src/runtime/touch.ts +76 -0
- package/src/scene/defineScene.ts +1396 -0
- package/src/scene/editorPlugins.ts +110 -0
- package/src/scene/gizmos.ts +148 -0
- package/src/scene/grammar.ts +120 -0
- package/src/scene/level.ts +296 -0
- package/src/scene/material.ts +188 -0
- package/src/server/auth/appConfig.ts +12 -0
- package/src/server/auth/global.ts +80 -0
- package/src/server/auth/host.ts +318 -0
- package/src/server/auth/models.ts +83 -0
- package/src/server/auth/types.ts +50 -0
- package/src/server/channel.ts +56 -0
- package/src/server/context.ts +36 -0
- package/src/server/db/defineDb.ts +237 -0
- package/src/server/db/fields.ts +132 -0
- package/src/server/db/httpTransport.ts +93 -0
- package/src/server/db/index.ts +7 -0
- package/src/server/db/marci/query.ts +412 -0
- package/src/server/db/types.ts +202 -0
- package/src/server/errors.ts +12 -0
- package/src/server/host.ts +74 -0
- package/src/server/inject.ts +13 -0
- package/src/server/runtime.ts +133 -0
- package/src/server/validate.ts +87 -0
- package/src/ui/NativeView.ts +142 -0
- package/src/ui/UI.ts +39 -0
- package/src/ui/UIBottomSheet.ts +139 -0
- package/src/ui/UIButton.ts +101 -0
- package/src/ui/UIContainer.ts +60 -0
- package/src/ui/UIImage.ts +83 -0
- package/src/ui/UIInput.ts +185 -0
- package/src/ui/UIModal.ts +139 -0
- package/src/ui/UINode.ts +830 -0
- package/src/ui/UIPager.ts +362 -0
- package/src/ui/UIPopover.ts +100 -0
- package/src/ui/UIScreen.ts +123 -0
- package/src/ui/UIScrollable.ts +87 -0
- package/src/ui/UISpacer.ts +14 -0
- package/src/ui/UITabs.ts +236 -0
- package/src/ui/UIText.ts +51 -0
- package/src/ui/UIVideo.ts +88 -0
- package/src/ui/UIVirtualizedList.ts +241 -0
- package/src/ui/UIWidget.ts +127 -0
- package/src/ui/fonts.ts +13 -0
- package/src/ui/presentable.ts +117 -0
- package/src/ui/router.ts +132 -0
- package/src/ui/theme.ts +84 -0
|
@@ -0,0 +1,233 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
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
|
+
}
|