lecodes-sdk 2.4.10 → 2.5.1
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/global.d.ts +35 -0
- package/dist/types/core/env.d.ts +25 -0
- package/dist/types/core/particlesConfig.d.ts +196 -0
- package/dist/types/g2/Font2D.d.ts +19 -0
- package/dist/types/g2/Material2D.d.ts +24 -0
- package/dist/types/g2/Node2D.d.ts +6 -5
- package/dist/types/g2/Particles2D.d.ts +169 -0
- package/dist/types/g2/Path2D.d.ts +95 -0
- package/dist/types/g2/Physics2D.d.ts +1 -1
- package/dist/types/g2/Scene2D.d.ts +15 -17
- package/dist/types/g2/Sprite.d.ts +12 -1
- package/dist/types/g2/SpriteSheet.d.ts +4 -1
- package/dist/types/g2/Text2D.d.ts +96 -0
- package/dist/types/g2/Tilemap.d.ts +8 -0
- package/dist/types/g2/defineScene2d.d.ts +69 -11
- package/dist/types/gl/Foliage.d.ts +0 -7
- package/dist/types/gl/Lightmap.d.ts +101 -2
- package/dist/types/gl/Material.d.ts +21 -15
- package/dist/types/gl/Model.d.ts +8 -3
- package/dist/types/gl/Particles.d.ts +3 -135
- package/dist/types/gl/Statics.d.ts +56 -0
- package/dist/types/gl/staticsFormat.d.ts +101 -0
- package/dist/types/inject.d.ts +11 -3
- package/dist/types/runtime/env.d.ts +10 -0
- package/dist/types/runtime/envValues.d.ts +8 -0
- package/dist/types/scene/defineScene.d.ts +17 -12
- package/dist/types/server/env.d.ts +14 -0
- package/dist/types/server/inject.d.ts +1 -0
- package/dist/types/ui/fonts.d.ts +4 -0
- package/dist/types/version.d.ts +1 -1
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/src/bridges/2d.d.ts +65 -5
- package/src/bridges/gl.d.ts +34 -15
- package/src/compile/bakeMacro.ts +3 -0
- package/src/compile/compileProject.ts +25 -4
- package/src/compile/envCompile.ts +74 -0
- package/src/compile/fontMacro.ts +11 -6
- package/src/compile/index.ts +6 -0
- package/src/compile/resourceSlots.ts +1 -0
- package/src/compile/serverSplit.ts +47 -4
- package/src/core/env.ts +47 -0
- package/src/core/particlesConfig.ts +422 -0
- package/src/g2/Font2D.ts +87 -0
- package/src/g2/Material2D.ts +64 -0
- package/src/g2/Node2D.ts +10 -6
- package/src/g2/Particles2D.ts +337 -0
- package/src/g2/Path2D.ts +215 -0
- package/src/g2/Physics2D.ts +1 -1
- package/src/g2/Scene2D.ts +16 -22
- package/src/g2/Sprite.ts +23 -4
- package/src/g2/SpriteSheet.ts +9 -5
- package/src/g2/Text2D.ts +178 -0
- package/src/g2/Tilemap.ts +10 -0
- package/src/g2/defineScene2d.ts +108 -21
- package/src/g2/groups2d.ts +2 -2
- package/src/gl/Foliage.ts +1 -16
- package/src/gl/Lightmap.ts +70 -26
- package/src/gl/Material.ts +51 -21
- package/src/gl/Model.ts +19 -5
- package/src/gl/Particles.ts +22 -361
- package/src/gl/Statics.ts +301 -0
- package/src/gl/nav/NavMesh.ts +4 -1
- package/src/gl/staticsFormat.ts +240 -0
- package/src/inject.ts +18 -3
- package/src/runtime/env.ts +13 -0
- package/src/runtime/envValues.ts +8 -0
- package/src/runtime/fetch.ts +18 -0
- package/src/scene/defineScene.ts +107 -27
- package/src/server/certs/russian-trusted-ca.pem +81 -0
- package/src/server/env.ts +25 -0
- package/src/server/host.ts +7 -0
- package/src/server/inject.ts +2 -1
- package/src/server/trustedCa.ts +23 -0
- package/src/ui/fonts.ts +46 -0
- package/src/version.ts +1 -1
package/src/runtime/fetch.ts
CHANGED
|
@@ -62,6 +62,24 @@ export const fetch = (url: string, options: FetchOptions = {}): Promise<FetchRes
|
|
|
62
62
|
export const fetchLocal = (path: string): FetchResponse =>
|
|
63
63
|
new FetchResponseImpl({ id: _creatorFetch.local(path) }, 200) // the host's buffer: not owned
|
|
64
64
|
|
|
65
|
+
// A local `asset()` compiles to `__localAsset("id:" + _creatorFetch.local(<name>), <name>)` (sdk/src/compile/
|
|
66
|
+
// compileProject.ts): the "id:" url is the host's buffer id, assigned in the order the bundle asks, so it names the
|
|
67
|
+
// asset for THIS run only; the staged NAME is what stays the same across compiles. The pair is kept so a cache
|
|
68
|
+
// written by one run (gl/Statics.ts) can name an asset to the next.
|
|
69
|
+
const localAssetNames = new Map<string, string>()
|
|
70
|
+
const localAssetUrls = new Map<string, string>()
|
|
71
|
+
/** @internal */
|
|
72
|
+
export const __localAsset = (url: string, name: string): string => {
|
|
73
|
+
localAssetNames.set(url, name)
|
|
74
|
+
localAssetUrls.set(name, url)
|
|
75
|
+
return url
|
|
76
|
+
}
|
|
77
|
+
/** @internal The stable name of an asset url: its staged name for a local asset, the url itself otherwise (a server
|
|
78
|
+
* build's urls are paths, stable on their own). */
|
|
79
|
+
export const assetNameOf = (url: string): string => localAssetNames.get(url) ?? url
|
|
80
|
+
/** @internal The url of a stable asset name in this run (the inverse of `assetNameOf`). */
|
|
81
|
+
export const assetUrlOf = (name: string): string => localAssetUrls.get(name) ?? name
|
|
82
|
+
|
|
65
83
|
/** A handle to a picked / fetched file (see openFilePicker). */
|
|
66
84
|
export class File {
|
|
67
85
|
/** @internal The host buffer (not owned — see the header). Same key `FetchResponse` carries, so
|
package/src/scene/defineScene.ts
CHANGED
|
@@ -49,10 +49,11 @@ import { CAMERA_DEFAULTS } from "../gl/Camera"
|
|
|
49
49
|
import { CameraPlace } from "../gl/CameraPlace"
|
|
50
50
|
import { Node } from "../gl/Node"
|
|
51
51
|
import { Mesh } from "../gl/Mesh"
|
|
52
|
-
import { Model } from "../gl/Model"
|
|
52
|
+
import { Model, foliageKindOf, type FoliageKind } from "../gl/Model"
|
|
53
53
|
import { Physics } from "../gl/physics/Physics"
|
|
54
54
|
import { CharacterController } from "../gl/physics/CharacterController"
|
|
55
|
-
import { Lightmap, type LightmapDef } from "../gl/Lightmap"
|
|
55
|
+
import { Lightmap, type LightmapDef, type LightmapPrepared } from "../gl/Lightmap"
|
|
56
|
+
import { coveredRoots, defsDigest, bakeDigest, findNode, open as staticsOpen, build as staticsBuild, snapshot as staticsSnapshot, staticsConfig, toolRun, type CoveredRoot, type OpenedCache } from "../gl/Statics"
|
|
56
57
|
import { Foliage, type FoliageOptions } from "../gl/Foliage"
|
|
57
58
|
import { NavMesh } from "../gl/nav/NavMesh"
|
|
58
59
|
import { Terrain, type TerrainLayer, type TerrainRegion, type TerrainOptions } from "../gl/terrain/Terrain"
|
|
@@ -191,19 +192,23 @@ export type SceneNodeDef = {
|
|
|
191
192
|
* the GLB instance, so `castShadows: false` on a first-person viewmodel covers the whole gun. */
|
|
192
193
|
castShadows?: boolean
|
|
193
194
|
receiveShadows?: boolean
|
|
194
|
-
/**
|
|
195
|
-
* receiver and an occluder
|
|
196
|
-
* Default: static unless a `Physics` aspect moves the node (`dynamic` —
|
|
197
|
-
* `kinematic`).
|
|
198
|
-
*
|
|
195
|
+
/** Is this node STATIC — part of the level that never moves once loaded? A static is what the bake sees (a
|
|
196
|
+
* receiver and an occluder, real-time shadow casting off once the bake applies) and what the level cache
|
|
197
|
+
* snapshots after the first load. Default: static unless a `Physics` aspect moves the node (`dynamic` —
|
|
198
|
+
* Physics' default — or `kinematic`). `false` = built live, lit in real time, never cached. Set it only to
|
|
199
|
+
* override that rule; prefab subtrees inherit the verdict. */
|
|
200
|
+
static?: boolean
|
|
201
|
+
/** `'cube'` = a static whose every mesh bakes as AMBIENT CUBES instead of atlas texels — an occluder (its shadow and
|
|
199
202
|
* bounce stay in the bake) lit where it stands, the way the bake treats small and thin props on its own: a pipe run, a
|
|
200
203
|
* fence, a prop whose lightmap seams show. A prefab marked `'cube'` bakes its statics that way too. By name for a whole
|
|
201
204
|
* kit: the `cube` option of `env.lightmap`'s `bake()`. */
|
|
202
|
-
lightmap?:
|
|
205
|
+
lightmap?: "cube"
|
|
203
206
|
/** `model` nodes: vegetation — load through the FOLIAGE tier (wind, touch bending, per-copy tint; see
|
|
204
|
-
* `Foliage`). `
|
|
205
|
-
*
|
|
206
|
-
|
|
207
|
+
* `Foliage`). `'tree'` (= `true`): a crown, lit by the sky and the real-time sun; `'grass'`: ground cover, lit by
|
|
208
|
+
* the terrain's baked light under it (`Model.load`'s `foliage`). The object form names the kind and may add
|
|
209
|
+
* `fade: [start, end]`: the cards thin out over that distance range (m) and the copy drops past `end` — for
|
|
210
|
+
* ground cover. The fade is per ASSET: every node of the same GLB shares it. */
|
|
211
|
+
foliage?: boolean | "tree" | "grass" | { kind?: "tree" | "grass", fade?: [number, number] }
|
|
207
212
|
/** Navigation (a scene with `env.navmesh`, see navmesh.md): every STATIC body is walkable by
|
|
208
213
|
* default. `false` leaves this one out of the bake; `'unwalkable'` cuts its footprint out
|
|
209
214
|
* (nobody stands on or crosses it). */
|
|
@@ -417,9 +422,9 @@ const movesByPhysics = (def: SceneNodeDef): boolean =>
|
|
|
417
422
|
((e.ctor as unknown) === Physics && ((e.props as { motion?: string } | undefined)?.motion ?? "dynamic") !== "static")
|
|
418
423
|
|| (e.ctor as unknown) === CharacterController)
|
|
419
424
|
|
|
420
|
-
/** The static verdict for one node def (the editor's "
|
|
421
|
-
*
|
|
422
|
-
export const
|
|
425
|
+
/** The static verdict for one node def (the editor's "Static" switch shows the same rule). Children of a
|
|
426
|
+
* mover are movers too — buildNodes carries that down, this is the node's own verdict. */
|
|
427
|
+
export const isStatic = (def: SceneNodeDef): boolean => def.static !== undefined ? def.static : !movesByPhysics(def)
|
|
423
428
|
|
|
424
429
|
/** A terrain node: from its file or a fresh flat grid; textures load before the node appears (like a
|
|
425
430
|
* mesh's material). Stamps and holes apply later in buildNodes, once every target node exists. */
|
|
@@ -443,23 +448,39 @@ const createTerrain = async (def: TerrainNodeDef, lightmap: boolean): Promise<No
|
|
|
443
448
|
// cannot leak into the next node that leaves the flag alone.
|
|
444
449
|
const modelTemplates = new Map<string, Promise<Model>>()
|
|
445
450
|
|
|
446
|
-
|
|
447
|
-
|
|
451
|
+
/** What one `load()` spent where — the `[load]` line instantiate prints (always on, like the host's
|
|
452
|
+
* `[startup]`): a slow open names its stage. `glb` is the wall from the first template's fetch to the last
|
|
453
|
+
* one's resolve (they overlap); `clone` / `aspects` are sums of synchronous work. */
|
|
454
|
+
const loadStats = { glb: 0, glbFirst: 0, glbLast: 0, clones: 0, cloneMs: 0, aspectsMs: 0 }
|
|
455
|
+
const resetLoadStats = (): void => { loadStats.glb = 0; loadStats.glbFirst = 0; loadStats.glbLast = 0; loadStats.clones = 0; loadStats.cloneMs = 0; loadStats.aspectsMs = 0 }
|
|
456
|
+
|
|
457
|
+
const templateModel = (source: string, lightmap: boolean, foliage: FoliageKind): Promise<Model> => {
|
|
458
|
+
const key = `${lightmap}\u0000${foliage || ""}\u0000${source}`
|
|
448
459
|
let template = modelTemplates.get(key)
|
|
449
460
|
if (template === undefined) {
|
|
450
|
-
|
|
461
|
+
if (loadStats.glb++ === 0) loadStats.glbFirst = Date.now()
|
|
462
|
+
template = Model.load(source, { lightmap, foliage }).then((m) => { loadStats.glbLast = Date.now(); return m })
|
|
451
463
|
modelTemplates.set(key, template)
|
|
452
464
|
// a failed load must not poison the cache — the next node retries
|
|
453
465
|
void template.catch(() => modelTemplates.delete(key))
|
|
454
466
|
}
|
|
455
|
-
return template
|
|
467
|
+
return template
|
|
456
468
|
}
|
|
457
469
|
|
|
470
|
+
const loadModel = (source: string, lightmap: boolean, foliage: FoliageKind): Promise<Model> =>
|
|
471
|
+
templateModel(source, lightmap, foliage).then((t) => {
|
|
472
|
+
const t0 = Date.now()
|
|
473
|
+
const c = t.clone()
|
|
474
|
+
loadStats.clones++
|
|
475
|
+
loadStats.cloneMs += Date.now() - t0
|
|
476
|
+
return c
|
|
477
|
+
})
|
|
478
|
+
|
|
458
479
|
/** @internal */
|
|
459
480
|
export const createSource = (path: string, def: SceneNodeDef, lightmap: boolean): Node | Promise<Node> => {
|
|
460
481
|
const sources = [ def.mesh, def.model, def.light, def.make, def.prefab, def.camera, def.terrain ].filter((s) => s !== undefined).length
|
|
461
482
|
if (sources > 1) throw new Error(`Scene node "${path}" declares more than one source (mesh/model/light/make/prefab/camera/terrain)`)
|
|
462
|
-
if (def.model !== undefined) return loadModel(def.model, lightmap,
|
|
483
|
+
if (def.model !== undefined) return loadModel(def.model, lightmap, foliageKindOf(def.foliage))
|
|
463
484
|
if (def.mesh !== undefined) return createMesh(def.mesh, def.material)
|
|
464
485
|
if (def.terrain !== undefined) return createTerrain(def.terrain, lightmap)
|
|
465
486
|
if (def.light !== undefined) return createLight(path, def.light)
|
|
@@ -848,7 +869,7 @@ const prefabPartRows = (defs: Record<string, SceneNodeDef>, local: Record<string
|
|
|
848
869
|
}
|
|
849
870
|
|
|
850
871
|
/** @internal */
|
|
851
|
-
export const attachPrefab = async (wrapper: Node, path: string, def: SceneNodeDef, scene: Scene, stack: Set<SceneDef>, lm: LightmapCtx | null): Promise<void> => {
|
|
872
|
+
export const attachPrefab = async (wrapper: Node, path: string, def: SceneNodeDef, scene: Scene, stack: Set<SceneDef>, lm: LightmapCtx | null, covered?: Map<string, CoveredRoot & { node?: Node }>): Promise<void> => {
|
|
852
873
|
const pdef = (def.prefab as { def?: SceneDef } | undefined)?.def
|
|
853
874
|
if (!pdef || typeof pdef !== "object") {
|
|
854
875
|
console.error(`[scene] "${path}".prefab is not a scene handle (import the .scene file's default export)`)
|
|
@@ -862,7 +883,7 @@ export const attachPrefab = async (wrapper: Node, path: string, def: SceneNodeDe
|
|
|
862
883
|
}
|
|
863
884
|
const local: Record<string, Node> = {}
|
|
864
885
|
const localRuns: EditorRun[] = [] // discarded: instance internals render, but aren't editor-tracked
|
|
865
|
-
await buildNodes(pdef.nodes ?? {}, wrapper, scene, local, localRuns, new Set(stack).add(pdef), lm)
|
|
886
|
+
await buildNodes(pdef.nodes ?? {}, wrapper, scene, local, localRuns, new Set(stack).add(pdef), lm, covered, `${path}/`)
|
|
866
887
|
const rows = prefabPartRows(pdef.nodes ?? {}, local, "", 0)
|
|
867
888
|
;(wrapper as { _prefabParts?: ModelPartRow[] })._prefabParts = rows
|
|
868
889
|
if (def.overrides) applyOverridesToRows(rows, def.overrides)
|
|
@@ -874,13 +895,13 @@ export const attachPrefab = async (wrapper: Node, path: string, def: SceneNodeDe
|
|
|
874
895
|
const buildNodes = async (
|
|
875
896
|
defs: Record<string, SceneNodeDef>, parent: Node | null, scene: Scene,
|
|
876
897
|
nodes: Record<string, Node>, editorRuns: EditorRun[], stack: Set<SceneDef>,
|
|
877
|
-
lm: LightmapCtx | null = null,
|
|
898
|
+
lm: LightmapCtx | null = null, covered: Map<string, CoveredRoot & { node?: Node }> = new Map(), coveredPrefix = "",
|
|
878
899
|
): Promise<void> => {
|
|
879
900
|
const pending: { path: string, node: Node, def: SceneNodeDef }[] = []
|
|
880
901
|
const moverRoots: Node[] = []
|
|
881
902
|
|
|
882
903
|
// `parentMoves`: a child of a mover moves with it (the body under the hero's capsule) — it never
|
|
883
|
-
// bakes, whatever its own def says short of an explicit `
|
|
904
|
+
// bakes, whatever its own def says short of an explicit `static: true`
|
|
884
905
|
const build = async (name: string, nd: SceneNodeDef, parentNode: Node | null, parentPath: string, parentMoves = false): Promise<void> => {
|
|
885
906
|
// names are path segments — '/' would fork the path, ':' would ambiguate editor card keys
|
|
886
907
|
if (name === "" || name.includes("/") || name.includes(":")) {
|
|
@@ -889,7 +910,19 @@ const buildNodes = async (
|
|
|
889
910
|
// the path is derived from def keys BEFORE any await, so it is deterministic even though
|
|
890
911
|
// Promise.all makes build completion (and record insertion) order nondeterministic
|
|
891
912
|
const path = parentPath === "" ? name : `${parentPath}/${name}`
|
|
892
|
-
|
|
913
|
+
// a subtree the static level cache rebuilds natively (gl/Statics.ts): one plain JS node, the clones under it
|
|
914
|
+
// (`coveredPrefix`: this build's full-path prefix — a prefab's internals are keyed from the level's root)
|
|
915
|
+
const cov = covered.size > 0 ? covered.get(coveredPrefix + path) : undefined
|
|
916
|
+
if (cov) {
|
|
917
|
+
const node = new Node()
|
|
918
|
+
if (parentNode) attachHost(parentNode, nd, path).add(node)
|
|
919
|
+
scene.add(node)
|
|
920
|
+
applyNode(node, name, nd)
|
|
921
|
+
cov.node = node
|
|
922
|
+
nodes[path] = node
|
|
923
|
+
return
|
|
924
|
+
}
|
|
925
|
+
const lmStatic = lm !== null && (nd.static !== undefined ? nd.static : !parentMoves && isStatic(nd))
|
|
893
926
|
const lmCube = lmStatic && (nd.lightmap === "cube" || lm!.cube === true)
|
|
894
927
|
// a mover in a level with a light volume reads the volume (models: the engine binds it; meshes: registered below)
|
|
895
928
|
const node = await createSource(path, nd, lmStatic)
|
|
@@ -905,7 +938,7 @@ const buildNodes = async (
|
|
|
905
938
|
if (lm !== null && !lmStatic && !parentMoves) moverRoots.push(node)
|
|
906
939
|
if (EDITOR && isEditMode()) addEditorMarker(node, nd)
|
|
907
940
|
// a prefab under a static wrapper bakes; under a mover its internals stay movers (volume-lit)
|
|
908
|
-
if (nd.prefab !== undefined) await attachPrefab(node, path, nd, scene, stack, lmStatic ? { prefix: `${lm!.prefix}${path}/`, cube: lmCube } : null)
|
|
941
|
+
if (nd.prefab !== undefined) await attachPrefab(node, path, nd, scene, stack, lmStatic ? { prefix: `${lm!.prefix}${path}/`, cube: lmCube } : null, covered.size > 0 ? covered : undefined)
|
|
909
942
|
pending.push({ path, node, def: nd })
|
|
910
943
|
nodes[path] = node
|
|
911
944
|
await Promise.all(Object.entries(nd.children ?? {}).map(([ childName, child ]) => build(childName, child, node, path, parentMoves || (lm !== null && !lmStatic))))
|
|
@@ -913,12 +946,14 @@ const buildNodes = async (
|
|
|
913
946
|
|
|
914
947
|
await Promise.all(Object.entries(defs).map(([ name, nd ]) => build(name, nd, parent, "")))
|
|
915
948
|
const makeWaits: Promise<void>[] = []
|
|
949
|
+
const tAspects = Date.now()
|
|
916
950
|
for (const p of pending) {
|
|
917
951
|
const aspectWait = attachAspects(p.node, p.path, p.def, nodes, scene, editorRuns)
|
|
918
952
|
if (aspectWait) makeWaits.push(aspectWait)
|
|
919
953
|
const wait = attachMake(p.node, p.path, p.def, nodes, scene, editorRuns)
|
|
920
954
|
if (wait) makeWaits.push(wait)
|
|
921
955
|
}
|
|
956
|
+
loadStats.aspectsMs += Date.now() - tAspects
|
|
922
957
|
if (makeWaits.length > 0) await Promise.all(makeWaits)
|
|
923
958
|
|
|
924
959
|
// terrain stamps + holes: every node of the file exists now (make() subtrees included), and a
|
|
@@ -973,7 +1008,29 @@ const instantiate = async (def: SceneDef, editorRuns: EditorRun[]): Promise<{ sc
|
|
|
973
1008
|
// baked lighting is a play-mode concern; the level owns the registry (a rebuild starts clean)
|
|
974
1009
|
const lm: LightmapCtx | null = lightmap && !(EDITOR && isEditMode()) ? { prefix: "" } : null
|
|
975
1010
|
if (lm) Lightmap.clear()
|
|
976
|
-
|
|
1011
|
+
resetLoadStats()
|
|
1012
|
+
const tLoad = Date.now()
|
|
1013
|
+
// THE STATIC LEVEL CACHE (gl/Statics.ts): with a fresh `<level>.static` beside the bake the covered subtree is
|
|
1014
|
+
// rebuilt natively and the JS builds the rest. Under a tool run (a bake, the snapshot itself) and in the editor
|
|
1015
|
+
// the level always builds the slow, complete way.
|
|
1016
|
+
const staticsCfg = staticsConfig()
|
|
1017
|
+
const roots = lm && lightmap && (staticsCfg || !toolRun()) ? coveredRoots(def.nodes, def) : []
|
|
1018
|
+
let prepared: LightmapPrepared | null = null
|
|
1019
|
+
let opened: OpenedCache | null = null
|
|
1020
|
+
if (roots.length > 0 && lightmap?.statics && lm && !staticsCfg && !toolRun()) {
|
|
1021
|
+
prepared = await Lightmap.prepare(scene, lightmap) // the pages first: the engine binds the records as it clones
|
|
1022
|
+
opened = await staticsOpen(lightmap.statics, defsDigest(roots, def), bakeDigest(prepared?.data ?? null))
|
|
1023
|
+
if (opened && opened.roots !== roots.length) { console.warn(`[static] the cache covers ${opened.roots} subtree(s), the scene has ${roots.length} — building the level the slow way`); opened = null }
|
|
1024
|
+
}
|
|
1025
|
+
const covered = new Map<string, CoveredRoot & { node?: Node }>()
|
|
1026
|
+
if (opened) for (const r of roots) covered.set(r.path, r)
|
|
1027
|
+
await buildNodes(def.nodes ?? {}, null, scene, nodes, editorRuns, new Set([ def ]), lm, covered)
|
|
1028
|
+
let cachedMade = 0
|
|
1029
|
+
if (opened) {
|
|
1030
|
+
const pages = prepared ? Lightmap.pageIds(prepared) : new Uint32Array(0)
|
|
1031
|
+
cachedMade = await staticsBuild(opened, scene, roots.map((r) => covered.get(r.path)!.node!), pages, templateModel)
|
|
1032
|
+
}
|
|
1033
|
+
const nodesMs = Date.now() - tLoad
|
|
977
1034
|
if (navmesh && !(EDITOR && isEditMode())) registerNavOverrides(def.nodes, nodes)
|
|
978
1035
|
|
|
979
1036
|
// a camera NODE wins over the top-level `camera:` block; in play mode it keeps driving the view
|
|
@@ -1004,12 +1061,35 @@ const instantiate = async (def: SceneDef, editorRuns: EditorRun[]): Promise<{ sc
|
|
|
1004
1061
|
}
|
|
1005
1062
|
|
|
1006
1063
|
// the level is complete: apply the bake — or, under `lecodes lightmap bake`, run it
|
|
1007
|
-
|
|
1064
|
+
const tLm = Date.now()
|
|
1065
|
+
if (lm && lightmap) { if (prepared) await Lightmap.apply(prepared); else await Lightmap.load(scene, lightmap) }
|
|
1066
|
+
const lightmapMs = Date.now() - tLm
|
|
1008
1067
|
// navigation: load the baked mesh — or, under `lecodes navmesh bake`, dump the level's collision
|
|
1009
1068
|
// geometry for it (the static bodies exist by now; edit mode never loads it — v1)
|
|
1069
|
+
const tNav = Date.now()
|
|
1010
1070
|
if (navmesh && !(EDITOR && isEditMode())) {
|
|
1011
1071
|
await NavMesh.load(scene, { data: navmesh.data, agent: navmesh.agent, areas: navmesh.areas })
|
|
1012
1072
|
}
|
|
1073
|
+
const navMs = Date.now() - tNav
|
|
1074
|
+
const glbMs = loadStats.glb > 0 ? Math.max(0, loadStats.glbLast - loadStats.glbFirst) : 0
|
|
1075
|
+
console.log(`[load] ${Date.now() - tLoad} ms: nodes ${nodesMs} (${Object.keys(nodes).length} top, glb ${loadStats.glb} in ${glbMs}, clones ${loadStats.clones} in ${loadStats.cloneMs}, aspects ${loadStats.aspectsMs}${cachedMade ? `, ${cachedMade} static placement(s) from the cache` : ""}), lightmap ${lightmapMs}, navmesh ${navMs}`)
|
|
1076
|
+
|
|
1077
|
+
// `lecodes static`: the level is complete and bound — snapshot the covered subtree beside the bake
|
|
1078
|
+
if (staticsCfg && lightmap) {
|
|
1079
|
+
const out = `${staticsCfg.root}/${lightmap.path.replace(/\.bake$/, ".static")}`
|
|
1080
|
+
const live = roots.map((r) => ({ root: r, node: findNode(nodes, r.path) }))
|
|
1081
|
+
const lost = live.filter((r) => !r.node)
|
|
1082
|
+
if (roots.length === 0) console.error("[static] error: nothing to cache — the level has no static subtree (models and their colliders under plain nodes)")
|
|
1083
|
+
else if (lost.length > 0) console.error(`[static] error: no live node for ${lost.map((r) => r.root.path).join(", ")}`)
|
|
1084
|
+
else if (!_creator.staticsWrite) console.error("[static] error: this host cannot write the cache (build the desktop host)")
|
|
1085
|
+
else {
|
|
1086
|
+
const bake = Lightmap._bakeData()
|
|
1087
|
+
const snap = staticsSnapshot(live as { root: CoveredRoot, node: Node }[], def, bake, defsDigest(roots, def))
|
|
1088
|
+
if (_creator.staticsWrite(out, snap.bytes)) console.log(`[static] wrote ${out}: ${snap.placements} placement(s) of ${snap.kinds} kind(s), ${snap.records} bake record(s), ${(snap.bytes.length / 1024).toFixed(0)} KB${bake ? "" : " (no bake bound)"}`)
|
|
1089
|
+
else console.error(`[static] error: could not write ${out}`)
|
|
1090
|
+
}
|
|
1091
|
+
console.log("[static] done")
|
|
1092
|
+
}
|
|
1013
1093
|
|
|
1014
1094
|
return { scene, nodes }
|
|
1015
1095
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Russian Trusted CA (the Ministry of Digital Development, Минцифры): what a server's fetch needs to reach
|
|
2
|
+
Russian services behind it — GigaChat (ngw.devices.sberbank.ru, gigachat.devices.sberbank.ru), Gosuslugi, Sber.
|
|
3
|
+
Source: https://gu-st.ru/content/Other/doc/russian_trusted_root_ca.cer + russian_trusted_sub_ca.cer (2026-11-03).
|
|
4
|
+
Root: CN=Russian Trusted Root CA, until 2032-02-27, sha256 D2:6D:2D:02:31:B7:C3:9F:92:CC:73:85:12:BA:54:10:35:19:E4:40:5D:68:B5:BD:70:3E:97:88:CA:8E:CF:31
|
|
5
|
+
Sub: CN=Russian Trusted Sub CA, until 2027-03-06, sha256 BB:BD:E2:10:3E:79:0B:99:9E:C6:2B:D0:3C:F6:25:A5:A2:E7:C3:16:E1:0A:FE:6A:49:0E:ED:EA:D8:B3:FD:9B
|
|
6
|
+
Loaded through NODE_EXTRA_CA_CERTS (src/server/trustedCa.ts): added to the defaults, never replacing them.
|
|
7
|
+
|
|
8
|
+
-----BEGIN CERTIFICATE-----
|
|
9
|
+
MIIFwjCCA6qgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCUlUx
|
|
10
|
+
PzA9BgNVBAoMNlRoZSBNaW5pc3RyeSBvZiBEaWdpdGFsIERldmVsb3BtZW50IGFu
|
|
11
|
+
ZCBDb21tdW5pY2F0aW9uczEgMB4GA1UEAwwXUnVzc2lhbiBUcnVzdGVkIFJvb3Qg
|
|
12
|
+
Q0EwHhcNMjIwMzAxMjEwNDE1WhcNMzIwMjI3MjEwNDE1WjBwMQswCQYDVQQGEwJS
|
|
13
|
+
VTE/MD0GA1UECgw2VGhlIE1pbmlzdHJ5IG9mIERpZ2l0YWwgRGV2ZWxvcG1lbnQg
|
|
14
|
+
YW5kIENvbW11bmljYXRpb25zMSAwHgYDVQQDDBdSdXNzaWFuIFRydXN0ZWQgUm9v
|
|
15
|
+
dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMfFOZ8pUAL3+r2n
|
|
16
|
+
qqE0Zp52selXsKGFYoG0GM5bwz1bSFtCt+AZQMhkWQheI3poZAToYJu69pHLKS6Q
|
|
17
|
+
XBiwBC1cvzYmUYKMYZC7jE5YhEU2bSL0mX7NaMxMDmH2/NwuOVRj8OImVa5s1F4U
|
|
18
|
+
zn4Kv3PFlDBjjSjXKVY9kmjUBsXQrIHeaqmUIsPIlNWUnimXS0I0abExqkbdrXbX
|
|
19
|
+
YwCOXhOO2pDUx3ckmJlCMUGacUTnylyQW2VsJIyIGA8V0xzdaeUXg0VZ6ZmNUr5Y
|
|
20
|
+
Ber/EAOLPb8NYpsAhJe2mXjMB/J9HNsoFMBFJ0lLOT/+dQvjbdRZoOT8eqJpWnVD
|
|
21
|
+
U+QL/qEZnz57N88OWM3rabJkRNdU/Z7x5SFIM9FrqtN8xewsiBWBI0K6XFuOBOTD
|
|
22
|
+
4V08o4TzJ8+Ccq5XlCUW2L48pZNCYuBDfBh7FxkB7qDgGDiaftEkZZfApRg2E+M9
|
|
23
|
+
G8wkNKTPLDc4wH0FDTijhgxR3Y4PiS1HL2Zhw7bD3CbslmEGgfnnZojNkJtcLeBH
|
|
24
|
+
BLa52/dSwNU4WWLubaYSiAmA9IUMX1/RpfpxOxd4Ykmhz97oFbUaDJFipIggx5sX
|
|
25
|
+
ePAlkTdWnv+RWBxlJwMQ25oEHmRguNYf4Zr/Rxr9cS93Y+mdXIZaBEE0KS2iLRqa
|
|
26
|
+
OiWBki9IMQU4phqPOBAaG7A+eP8PAgMBAAGjZjBkMB0GA1UdDgQWBBTh0YHlzlpf
|
|
27
|
+
BKrS6badZrHF+qwshzAfBgNVHSMEGDAWgBTh0YHlzlpfBKrS6badZrHF+qwshzAS
|
|
28
|
+
BgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsF
|
|
29
|
+
AAOCAgEAALIY1wkilt/urfEVM5vKzr6utOeDWCUczmWX/RX4ljpRdgF+5fAIS4vH
|
|
30
|
+
tmXkqpSCOVeWUrJV9QvZn6L227ZwuE15cWi8DCDal3Ue90WgAJJZMfTshN4OI8cq
|
|
31
|
+
W9E4EG9wglbEtMnObHlms8F3CHmrw3k6KmUkWGoa+/ENmcVl68u/cMRl1JbW2bM+
|
|
32
|
+
/3A+SAg2c6iPDlehczKx2oa95QW0SkPPWGuNA/CE8CpyANIhu9XFrj3RQ3EqeRcS
|
|
33
|
+
AQQod1RNuHpfETLU/A2gMmvn/w/sx7TB3W5BPs6rprOA37tutPq9u6FTZOcG1Oqj
|
|
34
|
+
C/B7yTqgI7rbyvox7DEXoX7rIiEqyNNUguTk/u3SZ4VXE2kmxdmSh3TQvybfbnXV
|
|
35
|
+
4JbCZVaqiZraqc7oZMnRoWrXRG3ztbnbes/9qhRGI7PqXqeKJBztxRTEVj8ONs1d
|
|
36
|
+
WN5szTwaPIvhkhO3CO5ErU2rVdUr89wKpNXbBODFKRtgxUT70YpmJ46VVaqdAhOZ
|
|
37
|
+
D9EUUn4YaeLaS8AjSF/h7UkjOibNc4qVDiPP+rkehFWM66PVnP1Msh93tc+taIfC
|
|
38
|
+
EYVMxjh8zNbFuoc7fzvvrFILLe7ifvEIUqSVIC/AzplM/Jxw7buXFeGP1qVCBEHq
|
|
39
|
+
391d/9RAfaZ12zkwFsl+IKwE/OZxW8AHa9i1p4GO0YSNuczzEm4=
|
|
40
|
+
-----END CERTIFICATE-----
|
|
41
|
+
-----BEGIN CERTIFICATE-----
|
|
42
|
+
MIIHQjCCBSqgAwIBAgICEAIwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCUlUx
|
|
43
|
+
PzA9BgNVBAoMNlRoZSBNaW5pc3RyeSBvZiBEaWdpdGFsIERldmVsb3BtZW50IGFu
|
|
44
|
+
ZCBDb21tdW5pY2F0aW9uczEgMB4GA1UEAwwXUnVzc2lhbiBUcnVzdGVkIFJvb3Qg
|
|
45
|
+
Q0EwHhcNMjIwMzAyMTEyNTE5WhcNMjcwMzA2MTEyNTE5WjBvMQswCQYDVQQGEwJS
|
|
46
|
+
VTE/MD0GA1UECgw2VGhlIE1pbmlzdHJ5IG9mIERpZ2l0YWwgRGV2ZWxvcG1lbnQg
|
|
47
|
+
YW5kIENvbW11bmljYXRpb25zMR8wHQYDVQQDDBZSdXNzaWFuIFRydXN0ZWQgU3Vi
|
|
48
|
+
IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9YPqBKOk19NFymrE
|
|
49
|
+
wehzrhBEgT2atLezpduB24mQ7CiOa/HVpFCDRZzdxqlh8drku408/tTmWzlNH/br
|
|
50
|
+
HuQhZ/miWKOf35lpKzjyBd6TPM23uAfJvEOQ2/dnKGGJbsUo1/udKSvxQwVHpVv3
|
|
51
|
+
S80OlluKfhWPDEXQpgyFqIzPoxIQTLZ0deirZwMVHarZ5u8HqHetRuAtmO2ZDGQn
|
|
52
|
+
vVOJYAjls+Hiueq7Lj7Oce7CQsTwVZeP+XQx28PAaEZ3y6sQEt6rL06ddpSdoTMp
|
|
53
|
+
BnCqTbxW+eWMyjkIn6t9GBtUV45yB1EkHNnj2Ex4GwCiN9T84QQjKSr+8f0psGrZ
|
|
54
|
+
vPbCbQAwNFJjisLixnjlGPLKa5vOmNwIh/LAyUW5DjpkCx004LPDuqPpFsKXNKpa
|
|
55
|
+
L2Dm6uc0x4Jo5m+gUTVORB6hOSzWnWDj2GWfomLzzyjG81DRGFBpco/O93zecsIN
|
|
56
|
+
3SL2Ysjpq1zdoS01CMYxie//9zWvYwzI25/OZigtnpCIrcd2j1Y6dMUFQAzAtHE+
|
|
57
|
+
qsXflSL8HIS+IJEFIQobLlYhHkoE3avgNx5jlu+OLYe0dF0Ykx1PGNjbwqvTX37R
|
|
58
|
+
Cn32NMjlotW2QcGEZhDKj+3urZizp5xdTPZitA+aEjZM/Ni71VOdiOP0igbw6asZ
|
|
59
|
+
2fxdozZ1TnSSYNYvNATwthNmZysCAwEAAaOCAeUwggHhMBIGA1UdEwEB/wQIMAYB
|
|
60
|
+
Af8CAQAwDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTR4XENCy2BTm6KSo9MI7NM
|
|
61
|
+
XqtpCzAfBgNVHSMEGDAWgBTh0YHlzlpfBKrS6badZrHF+qwshzCBxwYIKwYBBQUH
|
|
62
|
+
AQEEgbowgbcwOwYIKwYBBQUHMAKGL2h0dHA6Ly9yb3N0ZWxlY29tLnJ1L2NkcC9y
|
|
63
|
+
b290Y2Ffc3NsX3JzYTIwMjIuY3J0MDsGCCsGAQUFBzAChi9odHRwOi8vY29tcGFu
|
|
64
|
+
eS5ydC5ydS9jZHAvcm9vdGNhX3NzbF9yc2EyMDIyLmNydDA7BggrBgEFBQcwAoYv
|
|
65
|
+
aHR0cDovL3JlZXN0ci1wa2kucnUvY2RwL3Jvb3RjYV9zc2xfcnNhMjAyMi5jcnQw
|
|
66
|
+
gbAGA1UdHwSBqDCBpTA1oDOgMYYvaHR0cDovL3Jvc3RlbGVjb20ucnUvY2RwL3Jv
|
|
67
|
+
b3RjYV9zc2xfcnNhMjAyMi5jcmwwNaAzoDGGL2h0dHA6Ly9jb21wYW55LnJ0LnJ1
|
|
68
|
+
L2NkcC9yb290Y2Ffc3NsX3JzYTIwMjIuY3JsMDWgM6Axhi9odHRwOi8vcmVlc3Ry
|
|
69
|
+
LXBraS5ydS9jZHAvcm9vdGNhX3NzbF9yc2EyMDIyLmNybDANBgkqhkiG9w0BAQsF
|
|
70
|
+
AAOCAgEARBVzZls79AdiSCpar15dA5Hr/rrT4WbrOfzlpI+xrLeRPrUG6eUWIW4v
|
|
71
|
+
Sui1yx3iqGLCjPcKb+HOTwoRMbI6ytP/ndp3TlYua2advYBEhSvjs+4vDZNwXr/D
|
|
72
|
+
anbwIWdurZmViQRBDFebpkvnIvru/RpWud/5r624Wp8voZMRtj/cm6aI9LtvBfT9
|
|
73
|
+
cfzhOaexI/99c14dyiuk1+6QhdwKaCRTc1mdfNQmnfWNRbfWhWBlK3h4GGE9JK33
|
|
74
|
+
Gk8ZS8DMrkdAh0xby4xAQ/mSWAfWrBmfzlOqGyoB1U47WTOeqNbWkkoAP2ys94+s
|
|
75
|
+
Jg4NTkiDVtXRF6nr6fYi0bSOvOFg0IQrMXO2Y8gyg9ARdPJwKtvWX8VPADCYMiWH
|
|
76
|
+
h4n8bZokIrImVKLDQKHY4jCsND2HHdJfnrdL2YJw1qFskNO4cSNmZydw0Wkgjv9k
|
|
77
|
+
F+KxqrDKlB8MZu2Hclph6v/CZ0fQ9YuE8/lsHZ0Qc2HyiSMnvjgK5fDc3TD4fa8F
|
|
78
|
+
E8gMNurM+kV8PT8LNIM+4Zs+LKEV8nqRWBaxkIVJGekkVKO8xDBOG/aN62AZKHOe
|
|
79
|
+
GcyIdu7yNMMRihGVZCYr8rYiJoKiOzDqOkPkLOPdhtVlgnhowzHDxMHND/E2WA5p
|
|
80
|
+
ZHuNM/m0TXt2wTTPL7JH2YC0gPz/BvvSzjksgzU5rLbRyUKQkgU=
|
|
81
|
+
-----END CERTIFICATE-----
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `env` on the SERVER — every key of the project (src/core/env.ts says what `env` is). The values
|
|
3
|
+
* (Settings → Keys on le.codes, the project's `.env` on this machine) are handed to the bundle by the
|
|
4
|
+
* host that runs it (`setEnv`) through the `globalThis.__lecodesEnv` seam — a global on purpose,
|
|
5
|
+
* like `__lecodesRequest`: the bundle carries its own SDK copy, so the host can't pass a value by
|
|
6
|
+
* import. The compile lists the names the server code reads (serverSplit.ts `scanEnvReads`) so a
|
|
7
|
+
* deploy can warn before any call does.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { makeEnv, type Env } from "../core/env"
|
|
11
|
+
export { ENV_NAME, PUBLIC_ENV_PREFIX, isPublicEnvName, EnvError, type Env } from "../core/env"
|
|
12
|
+
|
|
13
|
+
const seam = globalThis as unknown as { __lecodesEnv?: Record<string, string> }
|
|
14
|
+
|
|
15
|
+
/** Host hook (runner / local backend / tests): the values the bundle reads. `null` = none set. */
|
|
16
|
+
export const setEnv = (values: Record<string, string> | null) => {
|
|
17
|
+
if (values) seam.__lecodesEnv = { ...values }
|
|
18
|
+
else delete seam.__lecodesEnv
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** @internal */
|
|
22
|
+
export const currentEnv = (): Record<string, string> => seam.__lecodesEnv ?? {}
|
|
23
|
+
|
|
24
|
+
/** The project's keys. A missing key throws `EnvError` on read. */
|
|
25
|
+
export const env: Env = makeEnv(currentEnv)
|
package/src/server/host.ts
CHANGED
|
@@ -10,6 +10,9 @@ export {
|
|
|
10
10
|
} from "./runtime"
|
|
11
11
|
export type { LoadedServer, InvokeResult, InvokerOptions, ServerRegistration, SeedOutcome } from "./runtime"
|
|
12
12
|
export type { RequestContext } from "./context"
|
|
13
|
+
export { setEnv, EnvError, ENV_NAME, PUBLIC_ENV_PREFIX, isPublicEnvName } from "./env"
|
|
14
|
+
export { TRUSTED_CA_FILE, trustedCaEnv } from "./trustedCa"
|
|
15
|
+
export type { Env } from "./env"
|
|
13
16
|
export { createChannelHub } from "./channelHub"
|
|
14
17
|
export type { ChannelHub, ChannelHubOptions, HubConnection, HubFanout } from "./channelHub"
|
|
15
18
|
export type { ChannelGroup, ClientFrame, ServerFrame } from "../runtime/wire"
|
|
@@ -61,6 +64,9 @@ export type ServerDescription = {
|
|
|
61
64
|
auth: { model: string | null }
|
|
62
65
|
/** `app.json` name (when the compile embedded one). */
|
|
63
66
|
appName?: string
|
|
67
|
+
/** The keys the server code reads (`env.NAME`, serverSplit.ts `scanEnvReads`): what a deploy checks
|
|
68
|
+
* against the keys that are set, so a missing one is a warning before it is a failed call. */
|
|
69
|
+
env: string[]
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
const describeField = (f: Field<any, any>): FieldDescription => {
|
|
@@ -107,5 +113,6 @@ export const describeServer = (server: LoadedServer): ServerDescription => {
|
|
|
107
113
|
pushChannels: [...server.pushChannels.keys()],
|
|
108
114
|
auth: { model: db?.$auth?.model ?? null },
|
|
109
115
|
appName: server.manifest.app?.name,
|
|
116
|
+
env: [...(server.manifest.env ?? [])],
|
|
110
117
|
}
|
|
111
118
|
}
|
package/src/server/inject.ts
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
* chisel injects these into the server bundle exactly like ./inject.ts does for the client bundle;
|
|
4
4
|
* nothing here may reach the client bundle. Keep this the ONLY file that exports server globals.
|
|
5
5
|
*
|
|
6
|
-
* Globals: t, model, defineDb, ref (schema + queries), ApiError, channel, pushChannel, request. Sign-in is
|
|
6
|
+
* Globals: t, model, defineDb, ref (schema + queries), ApiError, channel, pushChannel, request, env. Sign-in is
|
|
7
7
|
* not a global: it is `db.auth` of a db made with `defineDb({...}).withAuth({ model })`.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
export { t, model, defineDb, ref } from "./db"
|
|
11
11
|
export { ApiError } from "./errors"
|
|
12
|
+
export { env, type Env } from "./env"
|
|
12
13
|
export { channel } from "./channel"
|
|
13
14
|
export { pushChannel, type PushChannel, type PushMessage, type PushNotification, type PushTarget } from "./pushChannel"
|
|
14
15
|
export { request } from "./context"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The certificate authorities a project's server code can reach beyond the defaults: the Russian
|
|
3
|
+
* Trusted CA (the Ministry of Digital Development, Минцифры) — what GigaChat, Sber's and Gosuslugi's
|
|
4
|
+
* endpoints present (certs/russian-trusted-ca.pem, the root and the sub CA; their dates and
|
|
5
|
+
* fingerprints are in the file's header).
|
|
6
|
+
*
|
|
7
|
+
* Bun reads `NODE_EXTRA_CA_CERTS` once, at process start, and ADDS the file to its defaults (`tls.ca`
|
|
8
|
+
* on a fetch would replace them) — so the host that runs bundles sets the variable on the process
|
|
9
|
+
* before it starts: the runner's image (tools/runner/Dockerfile), the CLI's local backend (its child
|
|
10
|
+
* process, tools/cli/src/dev/localBackend.ts), Focus's check. Setting it from inside the process is
|
|
11
|
+
* too late. A variable already set is the operator's and stays: the variable names one file.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { dirname, join } from "node:path"
|
|
15
|
+
import { fileURLToPath } from "node:url"
|
|
16
|
+
|
|
17
|
+
/** The PEM of the Russian Trusted CA, by its absolute path in this SDK. */
|
|
18
|
+
export const TRUSTED_CA_FILE = join(dirname(fileURLToPath(import.meta.url)), "certs", "russian-trusted-ca.pem")
|
|
19
|
+
|
|
20
|
+
/** The environment a bundle-running process is started with so its fetch trusts the Russian Trusted CA
|
|
21
|
+
* too: `NODE_EXTRA_CA_CERTS` — unless `env` already names a file. */
|
|
22
|
+
export const trustedCaEnv = (env: Record<string, string | undefined> = process.env): Record<string, string> =>
|
|
23
|
+
env.NODE_EXTRA_CA_CERTS ? {} : { NODE_EXTRA_CA_CERTS: TRUSTED_CA_FILE }
|
package/src/ui/fonts.ts
CHANGED
|
@@ -11,3 +11,49 @@ export async function registerFont (fontFamily: string, url: string, options: Fo
|
|
|
11
11
|
_creatorTree.registerFont(fontFamily, url, options.weight ?? 400, options.style === "italic" ? 2 : 0, res, rej)
|
|
12
12
|
})
|
|
13
13
|
}
|
|
14
|
+
|
|
15
|
+
// ---- the font() macro's faces --------------------------------------------------------------------
|
|
16
|
+
// The `font("inter")` compile macro (compile/fontMacro.ts) emits one `__registerFont(...)` per face
|
|
17
|
+
// into a generated `.__fonts.ts` module. That registers the face with the UI tree (in the host's
|
|
18
|
+
// preferred container — woff2 on browsers) AND records its TTF URL here, so a `Font2D.load("Inter")`
|
|
19
|
+
// (whose native parser reads TTF/OTF only, never woff2) takes the same identifier UI text takes.
|
|
20
|
+
// The map is created on first use — no top-level side effect for chisel to drop.
|
|
21
|
+
|
|
22
|
+
type FaceRecord = { family: string, weight: number, italic: boolean, url: string }
|
|
23
|
+
|
|
24
|
+
let registeredFaces: Map<string, FaceRecord[]> | undefined // lower-cased family → its faces
|
|
25
|
+
const facesMap = (): Map<string, FaceRecord[]> => (registeredFaces ??= new Map())
|
|
26
|
+
|
|
27
|
+
/** The macro's registration call (a bundle global, like `__rpc`). `url` is either a full URL (a
|
|
28
|
+
* project / vendored entry) or a cdn base ending in "." — the format is appended here: the host's
|
|
29
|
+
* for the UI tree, always ttf for 2D text. */
|
|
30
|
+
export function __registerFont (family: string, url: string, weight: number, italic: boolean): void {
|
|
31
|
+
const base = url.endsWith(".")
|
|
32
|
+
const ttfUrl = base ? url + "ttf" : url
|
|
33
|
+
const key = family.toLowerCase()
|
|
34
|
+
const faces = facesMap().get(key) ?? []
|
|
35
|
+
const same = faces.find((f) => f.weight === weight && f.italic === italic)
|
|
36
|
+
if (same) same.url = ttfUrl
|
|
37
|
+
else faces.push({ family, weight, italic, url: ttfUrl })
|
|
38
|
+
facesMap().set(key, faces)
|
|
39
|
+
_creatorTree.registerFont(family, base ? url + (_creatorTree.fontFormat ?? "ttf") : url, weight, italic ? 2 : 0, () => {}, () => {})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** @internal The TTF URL of a `font()`-registered face: the exact (weight, italic) face first, else
|
|
43
|
+
* the nearest registered weight of that family in that style (a tie goes to the heavier), else
|
|
44
|
+
* undefined. Family names match case-insensitively. */
|
|
45
|
+
export function _fontUrlOf (family: string, weight = 400, italic = false): string | undefined {
|
|
46
|
+
const faces = registeredFaces?.get(family.toLowerCase())?.filter((f) => f.italic === italic)
|
|
47
|
+
if (!faces || faces.length === 0) return undefined
|
|
48
|
+
let best = faces[0]!
|
|
49
|
+
for (const f of faces) {
|
|
50
|
+
const d = Math.abs(f.weight - weight), bd = Math.abs(best.weight - weight)
|
|
51
|
+
if (d < bd || (d === bd && f.weight > best.weight)) best = f
|
|
52
|
+
}
|
|
53
|
+
return best.url
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** @internal The registered families (their declared spelling) — for "unknown family" errors. */
|
|
57
|
+
export function _registeredFontFamilies (): string[] {
|
|
58
|
+
return registeredFaces ? [ ...registeredFaces.values() ].map((faces) => faces[0]!.family) : []
|
|
59
|
+
}
|
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.
|
|
7
|
+
export const SDK_VERSION = "2.5.1"
|
|
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 => {
|