lecodes-sdk 2.2.11 → 2.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/global.d.ts +2 -0
- package/dist/host.d.ts +8 -2
- package/dist/types/core/Aspect.d.ts +5 -0
- package/dist/types/g2/Physics2D.d.ts +3 -2
- package/dist/types/g2/Scene2D.d.ts +6 -0
- package/dist/types/gl/Scene.d.ts +6 -0
- package/dist/types/gl/Texture.d.ts +2 -7
- package/dist/types/gl/animation/DynamicBone.d.ts +3 -0
- package/dist/types/gl/animation/Locomotion.d.ts +3 -0
- package/dist/types/gl/physics/Physics.d.ts +2 -1
- package/dist/types/gl/physics/Ragdoll.d.ts +4 -0
- package/dist/types/gl/vehicle/Vehicle.d.ts +4 -0
- package/dist/types/inject.d.ts +1 -1
- package/dist/types/runtime/app.d.ts +4 -2
- package/dist/types/runtime/fetch.d.ts +3 -0
- package/dist/types/runtime/storage.d.ts +6 -1
- package/dist/types/server/db/marci/query.d.ts +8 -2
- package/dist/types/ui/UIButton.d.ts +2 -2
- package/dist/types/ui/UIContainer.d.ts +4 -2
- package/dist/types/ui/UINode.d.ts +1 -1
- package/dist/types/ui/UIScreen.d.ts +2 -2
- package/dist/types/ui/UIVideo.d.ts +3 -2
- package/dist/types/ui/router.d.ts +1 -1
- package/dist/types/version.d.ts +1 -1
- package/dist/types.json +1 -1
- package/package.json +2 -5
- package/src/compile/assetIconMacro.ts +2 -1
- package/src/compile/assetMacro.ts +2 -1
- package/src/compile/compileProject.ts +28 -4
- package/src/compile/header.ts +17 -0
- package/src/compile/index.ts +2 -0
- package/src/core/Aspect.ts +2 -1
- package/src/g2/Physics2D.ts +6 -3
- package/src/g2/Scene2D.ts +9 -1
- package/src/gl/Scene.ts +9 -1
- package/src/gl/Texture.ts +2 -1
- package/src/gl/animation/DynamicBone.ts +6 -2
- package/src/gl/animation/Locomotion.ts +11 -1
- package/src/gl/physics/Physics.ts +3 -2
- package/src/gl/physics/Ragdoll.ts +8 -3
- package/src/gl/vehicle/Vehicle.ts +7 -2
- package/src/host.d.ts +8 -2
- package/src/inject.ts +1 -1
- package/src/runtime/app.ts +4 -2
- package/src/runtime/fetch.ts +6 -0
- package/src/runtime/storage.ts +14 -4
- package/src/server/db/marci/query.ts +8 -2
- package/src/ui/UIButton.ts +2 -2
- package/src/ui/UIContainer.ts +4 -2
- package/src/ui/UINode.ts +1 -1
- package/src/ui/UIScreen.ts +2 -2
- package/src/ui/UIVideo.ts +3 -2
- package/src/ui/router.ts +4 -3
- package/src/version.ts +1 -1
- package/prompts/2d.md +0 -391
- package/prompts/3d-scene-files.md +0 -113
- package/prompts/3d-scene.md +0 -122
- package/prompts/3d.md +0 -299
- package/prompts/README.md +0 -142
- package/prompts/ar.md +0 -142
- package/prompts/canvas.md +0 -28
- package/prompts/compose.ts +0 -46
- package/prompts/concept.md +0 -49
- package/prompts/core-design.md +0 -98
- package/prompts/core.md +0 -294
- package/prompts/design.md +0 -218
- package/prompts/dist/2d-game.md +0 -1567
- package/prompts/dist/3d-app.md +0 -1712
- package/prompts/dist/ar-app.md +0 -1585
- package/prompts/dist/concept.md +0 -49
- package/prompts/dist/design.md +0 -521
- package/prompts/dist/ui-app.md +0 -1175
- package/prompts/index.md +0 -39
- package/prompts/intake-prompt.md +0 -33
- package/prompts/namer-prompt.md +0 -12
- package/prompts/router-prompt.md +0 -50
- package/prompts/select.ts +0 -126
- package/prompts/ui-design.md +0 -203
- package/prompts/ui.md +0 -813
package/package.json
CHANGED
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
"./inject": "./src/inject.ts",
|
|
13
13
|
"./plugin": "./src/plugin.ts",
|
|
14
14
|
"./server": "./src/server/host.ts",
|
|
15
|
-
"./prompts/select": "./prompts/select.ts",
|
|
16
15
|
"./global.d.ts": "./dist/global.d.ts",
|
|
17
16
|
"./host.d.ts": "./dist/host.d.ts",
|
|
18
17
|
"./editor.d.ts": "./dist/editor.d.ts",
|
|
@@ -30,12 +29,11 @@
|
|
|
30
29
|
"test": "bun test --randomize",
|
|
31
30
|
"typecheck": "tsc --noEmit",
|
|
32
31
|
"docs:check": "node docs/check.mjs",
|
|
33
|
-
"prompts:build": "bun prompts/compose.ts",
|
|
34
32
|
"sync:marcidb": "bun scripts/sync-marcidb.ts",
|
|
35
33
|
"prepublishOnly": "bun run build"
|
|
36
34
|
},
|
|
37
35
|
"dependencies": {
|
|
38
|
-
"@letary/chisel": "^0.
|
|
36
|
+
"@letary/chisel": "^0.15.0"
|
|
39
37
|
},
|
|
40
38
|
"devDependencies": {
|
|
41
39
|
"@types/bun": "^1.3.14",
|
|
@@ -45,11 +43,10 @@
|
|
|
45
43
|
"marcidb-embedded": "^0.15.0",
|
|
46
44
|
"sharp": "^0.35.2"
|
|
47
45
|
},
|
|
48
|
-
"version": "2.2.
|
|
46
|
+
"version": "2.2.13",
|
|
49
47
|
"files": [
|
|
50
48
|
"src",
|
|
51
49
|
"dist",
|
|
52
|
-
"prompts",
|
|
53
50
|
"tests/helpers",
|
|
54
51
|
"README.md"
|
|
55
52
|
],
|
|
@@ -244,7 +244,8 @@ const iconExpr = (svg: string, call: IconCall): string => {
|
|
|
244
244
|
return call.tint ? `Object.assign(${src}, { tintColor: (${call.tint}) })` : src
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
-
|
|
247
|
+
/** Text into a template literal: backslash, backtick and `${` escaped. */
|
|
248
|
+
export const escapeTemplate = (s: string) => s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${")
|
|
248
249
|
|
|
249
250
|
// ---- the macro pass -----------------------------------------------------------------------------
|
|
250
251
|
|
|
@@ -92,7 +92,8 @@ export type AssetMacroRef = { path: string, index: number, length: number, line:
|
|
|
92
92
|
* image, a sound, a video, a model, a font, raw bytes) plus `.svg` (an SvgSource, what UIImage
|
|
93
93
|
* takes). Not `.json`, whose asset is its parsed data — a `.json` path in a string stays a string. */
|
|
94
94
|
const BARE_PATH_EXT = /\.(png|jpe?g|webp|gif|avif|bmp|svg|mp3|wav|ogg|m4a|aac|flac|mp4|webm|mov|m4v|glb|gltf|ktx2|hdr|exr|ttf|otf|woff2?|bin|bake|filamat)$/i
|
|
95
|
-
|
|
95
|
+
/** A module the bundler parses as code (and .json, which is data it understands): never a text asset. */
|
|
96
|
+
export const CODE_EXT = /\.([tj]sx?|mjs|cjs|json)$/i
|
|
96
97
|
// What precedes an import specifier: the literal is a module path, never a resource reference.
|
|
97
98
|
const IMPORT_BEFORE = /(?:\bfrom|\bimport|\bimport\s*\(|\brequire\s*\()\s*$/
|
|
98
99
|
|
|
@@ -8,6 +8,8 @@ import { detectEntry } from "./detectEntry"
|
|
|
8
8
|
import { buildHeader } from "./header"
|
|
9
9
|
import { inlineSourceMapComment, offsetSourceMap } from "./sourcemap"
|
|
10
10
|
import { ClientPublishError, clientPublishes, stubServerEntries } from "./serverSplit"
|
|
11
|
+
import { CODE_EXT } from "./assetMacro"
|
|
12
|
+
import { escapeTemplate } from "./assetIconMacro"
|
|
11
13
|
|
|
12
14
|
export type CompileFileType = "text" | "resource" | "shader"
|
|
13
15
|
|
|
@@ -145,12 +147,20 @@ export const entriesToFiles = (entries: CompileEntry[], opts: { publicUrl: strin
|
|
|
145
147
|
files[e.path] = `export default ""` // local-only shader with no compiled artifact yet
|
|
146
148
|
}
|
|
147
149
|
} else if (e.path.toLowerCase().endsWith(".svg")) {
|
|
148
|
-
|
|
150
|
+
// The file's text sits in a template literal: a backtick, `${` or a backslash in it (an
|
|
151
|
+
// editor's `<!-- exported by \`tool\` -->` comment) would otherwise end the literal and fail
|
|
152
|
+
// the compile with a chisel parse error that names nothing a person can act on.
|
|
153
|
+
files[e.path] = `export default SvgSource(\`${escapeTemplate(e.text ?? "")}\`)`
|
|
149
154
|
} else if (e.path.toLowerCase().endsWith(".json")) {
|
|
150
155
|
// A .json project file is DATA, not code: `import data from "./x.json"` — and `asset("./x.json")`,
|
|
151
156
|
// which desugars to that import — yields the parsed value. The bundler would otherwise try to
|
|
152
157
|
// parse the file as JS. JSON is an expression, so a default export is the whole translation.
|
|
153
158
|
files[e.path] = `export default ${e.text?.trim() || "null"}`
|
|
159
|
+
} else if (!CODE_EXT.test(e.path)) {
|
|
160
|
+
// Any other text file (.txt, .csv, .md, .css, .html, .xml, .yml …) is its TEXT: `asset("./levels.csv")`
|
|
161
|
+
// yields the string. Handing the raw text to the bundler as a module parsed as JS failed at
|
|
162
|
+
// run time instead (`__asset_0 is not defined`), after a compile that said nothing.
|
|
163
|
+
files[e.path] = `export default ${JSON.stringify(e.text ?? "")}`
|
|
154
164
|
} else {
|
|
155
165
|
files[e.path] = e.text ?? ""
|
|
156
166
|
}
|
|
@@ -158,13 +168,27 @@ export const entriesToFiles = (entries: CompileEntry[], opts: { publicUrl: strin
|
|
|
158
168
|
return files
|
|
159
169
|
}
|
|
160
170
|
|
|
161
|
-
/**
|
|
171
|
+
/** app.json's `"plugins"` in either shape — `{ "camera": "*" }` (id → version spec) or
|
|
172
|
+
* `["camera"]` (ids, every version `*`) — as id → spec. Anything else is no plugin. The list
|
|
173
|
+
* form is what CLAUDE.md and most hands write first; reading it as "no plugins" dropped the
|
|
174
|
+
* plugin from a web build and a shell with one warning nobody tied to the shape. */
|
|
175
|
+
export const pluginSpecsOf = (plugins: unknown): Record<string, string> => {
|
|
176
|
+
if (Array.isArray(plugins)) return Object.fromEntries(plugins.filter((p): p is string => typeof p === "string").map((id) => [id, "*"]))
|
|
177
|
+
if (plugins && typeof plugins === "object") {
|
|
178
|
+
return Object.fromEntries(Object.entries(plugins as Record<string, unknown>).map(([id, v]) => [id, typeof v === "string" ? v : "*"]))
|
|
179
|
+
}
|
|
180
|
+
return {}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** The ids of app.json's `"plugins"` (either shape), in its order. */
|
|
184
|
+
export const pluginIdsOf = (plugins: unknown): string[] => Object.keys(pluginSpecsOf(plugins))
|
|
185
|
+
|
|
186
|
+
/** The ids app.json lists under `"plugins"`, in its order. */
|
|
162
187
|
export const pluginsOfAppJson = (entries: CompileEntry[]): string[] => {
|
|
163
188
|
const app = entries.find(e => e.path.replace(/^[./\\]+/, "") === "app.json")
|
|
164
189
|
if (!app?.text) return []
|
|
165
190
|
try {
|
|
166
|
-
|
|
167
|
-
return plugins && typeof plugins === "object" && !Array.isArray(plugins) ? Object.keys(plugins) : []
|
|
191
|
+
return pluginIdsOf((JSON.parse(app.text) as { plugins?: unknown }).plugins)
|
|
168
192
|
} catch {
|
|
169
193
|
return [] // a broken app.json is reported by whoever reads it as a config
|
|
170
194
|
}
|
package/src/compile/header.ts
CHANGED
|
@@ -28,6 +28,18 @@ export type HeaderOptions = {
|
|
|
28
28
|
plugins?: string[]
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/** The optional engines of the game stack, by the bundled bridge calls that mean "in use": a body,
|
|
32
|
+
* a character, a vehicle or a ragdoll created, a ray cast, a sound loaded or played, the net
|
|
33
|
+
* bridge named, a navmesh call. Not the calls every 3D bundle carries (Node's body pose setters,
|
|
34
|
+
* Scene's audio listener). A `Physics.supported` probe pulls the Physics class whole, so it
|
|
35
|
+
* counts as use — the full variant is the safe side of that line. */
|
|
36
|
+
export const ENGINE_SIGNALS: readonly (readonly [name: "physics" | "net" | "nav" | "audio", re: RegExp])[] = [
|
|
37
|
+
["physics", /_creator\.(?:physicsCreateBody|physicsConfigure|physicsRaycast|characterCreate|vehicleCreate|ragdollCreate)\(/],
|
|
38
|
+
["net", /\b_creatorNet\b/], // the net core takes the bridge into a local once (core.ts bridge()): its calls never name it
|
|
39
|
+
["nav", /_creatorNav\.\w+\(/],
|
|
40
|
+
["audio", /_creatorAudio\.(?:loadClip|play)\(/], // a sound loaded or played; Scene's listener setters ride in every 3D bundle
|
|
41
|
+
]
|
|
42
|
+
|
|
31
43
|
export const buildHeader = (name: string, code: string, opts: HeaderOptions = {}): string => {
|
|
32
44
|
let header = `// sdk: ${SDK_VERSION}\n`
|
|
33
45
|
header += `// name: ${name.slice(0, 20)}\n`
|
|
@@ -51,6 +63,11 @@ export const buildHeader = (name: string, code: string, opts: HeaderOptions = {}
|
|
|
51
63
|
// bundle, and nothing 2D works without a Scene2D anyway.
|
|
52
64
|
if (code.includes("_creator2d.createScene")) header += `// 2d: enable\n`
|
|
53
65
|
|
|
66
|
+
// The game stack a shell's SDK variant must carry (the `3d` variant has no Jolt, net, nav or
|
|
67
|
+
// game audio; `2d` / `core` no net or nav): the CLI's resolveVariant reads these lines, so a 3D
|
|
68
|
+
// game with physics no longer gets a variant where Physics.configure throws on the device.
|
|
69
|
+
for (const [name, re] of ENGINE_SIGNALS) if (re.test(code)) header += `// ${name}: enable\n`
|
|
70
|
+
|
|
54
71
|
if (usesAR(code, opts.plugins)) header += `// ar: enable\n`
|
|
55
72
|
|
|
56
73
|
// Short, so with the engine lines: a host reads it before it registers anything.
|
package/src/compile/index.ts
CHANGED
package/src/core/Aspect.ts
CHANGED
|
@@ -403,7 +403,8 @@ export abstract class AspectHost<E extends EventMap = EventMap> extends Emitter<
|
|
|
403
403
|
}
|
|
404
404
|
|
|
405
405
|
// ---- systems: aspects of a scene --------------------------------------------
|
|
406
|
-
/**
|
|
406
|
+
/** What a scene needs to carry systems: Scene and Scene2D implement it over the helpers below. Public
|
|
407
|
+
* (never stripped from the public types: the `System` type names it). */
|
|
407
408
|
export type SystemHost = { readonly _systems: Map<Function, Aspect<any, any, any>> }
|
|
408
409
|
|
|
409
410
|
/** @internal `scene.system(Ctor, opts)`. */
|
package/src/g2/Physics2D.ts
CHANGED
|
@@ -231,9 +231,12 @@ export class Physics2D extends Aspect<"physics", Node2D> {
|
|
|
231
231
|
}
|
|
232
232
|
private static _gravity: [number, number] = [ 0, -980 ]
|
|
233
233
|
|
|
234
|
-
/** Whether this
|
|
235
|
-
*
|
|
236
|
-
|
|
234
|
+
/** Whether this host has 2D physics (a 2D engine built with CREATOR_2D_PHYSICS). False, never a
|
|
235
|
+
* throw, where there is no 2D engine at all (Web Lite, a 3d / core engine variant). It reports
|
|
236
|
+
* the build, not the world — there is nothing else to guard, since the world creates itself. */
|
|
237
|
+
static get supported(): boolean {
|
|
238
|
+
return typeof _creator2d !== "undefined" && !!_creator2d && typeof _creator2d.physicsHasSupport === "function" && _creator2d.physicsHasSupport()
|
|
239
|
+
}
|
|
237
240
|
|
|
238
241
|
/** Render interpolation of body transforms between the fixed 60 Hz steps (global; default on). Turn
|
|
239
242
|
* off to save per-frame transform writes when you have many moving bodies / don't need >60 Hz
|
package/src/g2/Scene2D.ts
CHANGED
|
@@ -62,6 +62,12 @@ export class Scene2D implements Presentable {
|
|
|
62
62
|
private static _active: Scene2D | null = null
|
|
63
63
|
static get active(): Scene2D | null { return Scene2D._active }
|
|
64
64
|
|
|
65
|
+
/** Whether this host has the 2D engine. False on Web Lite (UI and 3D only) and in a 3d / core
|
|
66
|
+
* engine variant of an app built with `lecodes app`; there `new Scene2D()` throws. */
|
|
67
|
+
static get supported(): boolean {
|
|
68
|
+
return typeof _creator2d !== "undefined" && !!_creator2d && typeof _creator2d.createScene === "function"
|
|
69
|
+
}
|
|
70
|
+
|
|
65
71
|
/** @internal — scene-level pointer listeners (the catch-all; target = hit node or null). */
|
|
66
72
|
readonly _clickListeners: Array<(ev: ClickEvent<Node2D | null>) => void> = []
|
|
67
73
|
readonly _touchStartListeners: Array<(ev: TouchStartEvent<Node2D | null>) => void> = []
|
|
@@ -70,6 +76,7 @@ export class Scene2D implements Presentable {
|
|
|
70
76
|
private readonly _filter?: 'nearest' | 'linear'
|
|
71
77
|
|
|
72
78
|
constructor(options: Scene2DOptions = {}) {
|
|
79
|
+
if (!Scene2D.supported) throw new Error("Scene2D: this host has no 2D engine (Web Lite runs UI and 3D apps; a 3d / core engine variant has no 2D) — check Scene2D.supported before creating one")
|
|
73
80
|
installRenderSyncedFrames()
|
|
74
81
|
this.id = _creator2d.createScene()
|
|
75
82
|
this.camera = new Camera2D(this.id)
|
|
@@ -121,7 +128,8 @@ export class Scene2D implements Presentable {
|
|
|
121
128
|
}
|
|
122
129
|
|
|
123
130
|
// ---- systems: aspects of the scene (core/Aspect.ts `System<'x', Scene2D>`) ----
|
|
124
|
-
/**
|
|
131
|
+
/** class → instance, the authoritative store (named accessors mirror this). Kept in the public
|
|
132
|
+
* types: it is what makes a Scene2D a `SystemHost` there. */
|
|
125
133
|
readonly _systems = new Map<Function, Aspect<any, any, any>>()
|
|
126
134
|
|
|
127
135
|
/** Attach (and configure) a system, or reconfigure it if already present. Returns the scene typed
|
package/src/gl/Scene.ts
CHANGED
|
@@ -206,7 +206,14 @@ export class Scene implements Presentable {
|
|
|
206
206
|
private static _active: Scene | null = null
|
|
207
207
|
static get active(): Scene | null { return Scene._active }
|
|
208
208
|
|
|
209
|
+
/** Whether this host has the 3D engine. False where the bundle was marked `gl: disable` (no scene
|
|
210
|
+
* created anywhere in it) and in a 2d / core engine variant; there `new Scene()` throws. */
|
|
211
|
+
static get supported(): boolean {
|
|
212
|
+
return typeof _creator !== "undefined" && !!_creator && typeof _creator.createScene === "function"
|
|
213
|
+
}
|
|
214
|
+
|
|
209
215
|
constructor(options: SceneOptions = {}) {
|
|
216
|
+
if (!Scene.supported) throw new Error("Scene: this host has no 3D engine (a 2d / core engine variant, or a bundle that creates no scene) — check Scene.supported before creating one")
|
|
210
217
|
installRenderSyncedFrames()
|
|
211
218
|
const parent = (options as any)._parent as Scene | undefined
|
|
212
219
|
if (parent) {
|
|
@@ -366,7 +373,8 @@ export class Scene implements Presentable {
|
|
|
366
373
|
}
|
|
367
374
|
|
|
368
375
|
// ---- systems: aspects of the scene (core/Aspect.ts `System`) ----
|
|
369
|
-
/**
|
|
376
|
+
/** class → instance, the authoritative store (named accessors mirror this). Kept in the public
|
|
377
|
+
* types: it is what makes a Scene a `SystemHost` there. */
|
|
370
378
|
readonly _systems = new Map<Function, Aspect<any, any, any>>()
|
|
371
379
|
|
|
372
380
|
/** Attach (and configure) a system, or reconfigure it if already present. Returns the scene typed
|
package/src/gl/Texture.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// A 3D GPU texture, decoded natively by creator-gl. Load from a URL or an already-fetched response.
|
|
2
2
|
|
|
3
3
|
import { fetch, type FetchResponse, type File } from "../runtime/fetch"
|
|
4
|
+
import type { Canvas } from "../canvas/Canvas"
|
|
4
5
|
|
|
5
6
|
/** `Texture.load` options. */
|
|
6
7
|
export type TextureLoadOptions = {
|
|
@@ -57,7 +58,7 @@ export class Texture {
|
|
|
57
58
|
// Wrap a Canvas's baked surface as a 3D texture (RGBA8, already rasterized → synchronous, no decode).
|
|
58
59
|
// The id is cached on the canvas (opaque _tex3dId), so backing several materials shares one texture and
|
|
59
60
|
// canvas.update() re-uploads it. Structural type avoids a Canvas import (keeps 3D free of 2D-canvas code).
|
|
60
|
-
static fromCanvas(canvas:
|
|
61
|
+
static fromCanvas(canvas: Canvas): Texture {
|
|
61
62
|
if (!canvas._tex3dId) {
|
|
62
63
|
const surfaceId = canvas._bakeSurface()
|
|
63
64
|
canvas._tex3dId = _creator.createTextureFromCanvas(surfaceId)
|
|
@@ -54,8 +54,8 @@ const FLOOR_NONE = 0, FLOOR_PLANE = 1, FLOOR_PROBE = 2
|
|
|
54
54
|
|
|
55
55
|
let warned = false
|
|
56
56
|
const supported = (): boolean => {
|
|
57
|
-
if (
|
|
58
|
-
if (!warned) { warned = true; console.warn("DynamicBone
|
|
57
|
+
if (DynamicBone.supported) return true
|
|
58
|
+
if (!warned) { warned = true; console.warn("DynamicBone: this host has no dynamic bones (DynamicBone.supported is false) — the animation shows as is") }
|
|
59
59
|
return false
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -134,6 +134,10 @@ export class DynamicBoneCollider extends Aspect<"dynamicBoneCollider", Node> {
|
|
|
134
134
|
export class DynamicBone extends Aspect<"dynamicBone", Node> {
|
|
135
135
|
static readonly aspect = "dynamicBone"
|
|
136
136
|
|
|
137
|
+
/** Whether this host simulates dynamic bones. False, never a throw, on Web Lite and in headless
|
|
138
|
+
* tests; there the aspect attaches, warns once, and `chain` stays empty. */
|
|
139
|
+
static get supported(): boolean { return typeof _creator !== "undefined" && !!_creator && typeof _creator.dynamicBoneCreate === "function" }
|
|
140
|
+
|
|
137
141
|
/** Only these bones (by name) join the chain — the root always does, a bone left out takes its
|
|
138
142
|
* subtree with it. Default: the root's whole subtree. */
|
|
139
143
|
bones?: string[]
|
|
@@ -225,9 +225,15 @@ export type LocomotionEvents = {
|
|
|
225
225
|
state: (state: LocomotionState) => void
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
let locoWarned = false
|
|
229
|
+
|
|
228
230
|
export class Locomotion extends Aspect<"loco", Node, LocomotionEvents> {
|
|
229
231
|
static readonly aspect = "loco"
|
|
230
232
|
|
|
233
|
+
/** Whether this host runs the locomotion selector. False, never a throw, on Web Lite and in
|
|
234
|
+
* headless tests; there the aspect attaches, warns once, and the body stays `'idle'`. */
|
|
235
|
+
static get supported(): boolean { return typeof _creator !== "undefined" && !!_creator && typeof _creator.locoCreate === "function" }
|
|
236
|
+
|
|
231
237
|
/** The animator whose clips are shown. Default: the first one on this node or under it (a Model
|
|
232
238
|
* child), which is the usual character — a capsule node with the model inside. */
|
|
233
239
|
animator?: Animator
|
|
@@ -281,7 +287,11 @@ export class Locomotion extends Aspect<"loco", Node, LocomotionEvents> {
|
|
|
281
287
|
* walk, a slowed-down character). */
|
|
282
288
|
rebuild(): void {
|
|
283
289
|
const anim = this.anim
|
|
284
|
-
if (!
|
|
290
|
+
if (!Locomotion.supported) {
|
|
291
|
+
if (!locoWarned) { locoWarned = true; console.warn("Locomotion: this host has no locomotion engine (Locomotion.supported is false) — the character stays idle") }
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
if (!anim) return
|
|
285
295
|
if (!this._loco) {
|
|
286
296
|
const first = this.set.idle ?? (this.set.gaits ?? [])[0] // binding a clip is what creates the native animator
|
|
287
297
|
if (first !== undefined) anim._slot(name(first))
|
|
@@ -209,9 +209,10 @@ export class Physics extends Aspect<"physics", Node> implements CompWriter {
|
|
|
209
209
|
_creator.physicsConfigure(cx(g), cy(g), cz(g), config.maxBodies ?? 0)
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
/** Whether this
|
|
212
|
+
/** Whether this host has 3D physics (an engine built with CREATOR_GL_PHYSICS). False, never a
|
|
213
|
+
* throw, where there is no 3D engine at all. */
|
|
213
214
|
static get supported(): boolean {
|
|
214
|
-
return !!_creator.physicsHasSupport && _creator.physicsHasSupport()
|
|
215
|
+
return typeof _creator !== "undefined" && !!_creator && typeof _creator.physicsHasSupport === "function" && _creator.physicsHasSupport()
|
|
215
216
|
}
|
|
216
217
|
|
|
217
218
|
/** Closest pickable body hit by the ray from `origin` along `dir` (up to `maxDist`), or null. */
|
|
@@ -45,7 +45,7 @@ import type { FieldMeta } from "../../core/fields"
|
|
|
45
45
|
import { Mathf } from "../../math/Mathf"
|
|
46
46
|
import { cx, cy, cz, Vec3, type Vec3Like } from "../../math/vec"
|
|
47
47
|
import { Node } from "../Node"
|
|
48
|
-
import { DEFAULT_FRICTION } from "./Physics"
|
|
48
|
+
import { DEFAULT_FRICTION, Physics } from "./Physics"
|
|
49
49
|
|
|
50
50
|
export interface RagdollHinge {
|
|
51
51
|
/** The hinge axis in the MODEL's space (its rest pose: +Z forward, +Y up, +X the model's left in
|
|
@@ -169,6 +169,11 @@ export const humanoidParts = (find: (role: string) => string | null): RagdollPar
|
|
|
169
169
|
export class Ragdoll extends Aspect<"ragdoll", Node> {
|
|
170
170
|
static readonly aspect = "ragdoll"
|
|
171
171
|
|
|
172
|
+
/** Whether this host can drop a body: 3D physics with ragdoll parts. False, never a throw,
|
|
173
|
+
* without physics or on an older host. A host that has it can still refuse the parts
|
|
174
|
+
* (`activate()` returns false). */
|
|
175
|
+
static get supported(): boolean { return Physics.supported && typeof _creator.ragdollCreate === "function" }
|
|
176
|
+
|
|
172
177
|
/** The parts: `'humanoid'` (default) finds the standard bones by name; a list places bodies on any
|
|
173
178
|
* bones — parents before children. */
|
|
174
179
|
parts: RagdollPart[] | "humanoid" = "humanoid"
|
|
@@ -243,9 +248,9 @@ export class Ragdoll extends Aspect<"ragdoll", Node> {
|
|
|
243
248
|
private _release: { left: number; blend: number } | null = null
|
|
244
249
|
|
|
245
250
|
onAttach(): void {
|
|
246
|
-
if (!
|
|
251
|
+
if (!Physics.supported) return
|
|
247
252
|
if (!_creator.ragdollCreate) {
|
|
248
|
-
console.warn("Ragdoll
|
|
253
|
+
console.warn("Ragdoll: this host has no ragdoll parts (Ragdoll.supported is false) — the body will not fall")
|
|
249
254
|
return
|
|
250
255
|
}
|
|
251
256
|
const find = (role: string): string | null => {
|
|
@@ -194,6 +194,11 @@ const clamp = (v: number, lo: number, hi: number): number => (v < lo ? lo : v >
|
|
|
194
194
|
export class Vehicle extends Aspect<"vehicle", Node> {
|
|
195
195
|
static readonly aspect = "vehicle"
|
|
196
196
|
|
|
197
|
+
/** Whether this host can drive a car: 3D physics with the vehicle model. False, never a throw,
|
|
198
|
+
* without physics or on an older host. A host that has it can still refuse a car's settings
|
|
199
|
+
* (`id` stays 0, with a warning). */
|
|
200
|
+
static get supported(): boolean { return Physics.supported && typeof _creator.vehicleCreate === "function" }
|
|
201
|
+
|
|
197
202
|
/** Chassis mass in kg (default 1200). Wheels are massless in this model. */
|
|
198
203
|
mass = 1200
|
|
199
204
|
/** Centre of mass height in chassis space (metres, negative = lower). Left out, it drops to halfway
|
|
@@ -274,9 +279,9 @@ export class Vehicle extends Aspect<"vehicle", Node> {
|
|
|
274
279
|
private _retunePending = false
|
|
275
280
|
|
|
276
281
|
onAttach(): void {
|
|
277
|
-
if (!
|
|
282
|
+
if (!Physics.supported) return
|
|
278
283
|
if (!_creator.vehicleCreate) {
|
|
279
|
-
console.warn("Vehicle
|
|
284
|
+
console.warn("Vehicle: this host has no vehicle model (Vehicle.supported is false) — the car will not move")
|
|
280
285
|
return
|
|
281
286
|
}
|
|
282
287
|
const shape = this.node.get(Shape)
|
package/src/host.d.ts
CHANGED
|
@@ -6,8 +6,11 @@
|
|
|
6
6
|
import type { SvgSource } from "./runtime/misc"
|
|
7
7
|
|
|
8
8
|
declare global {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
/** As in the browser, but the delay is REQUIRED (the native runtime throws without it) and no
|
|
10
|
+
* extra argument reaches the handler. Timers tick with the frame on every native host: one
|
|
11
|
+
* frame is the resolution. */
|
|
12
|
+
function setTimeout(handler: () => void, timeout: number): number
|
|
13
|
+
function setInterval(handler: () => void, timeout: number): number
|
|
11
14
|
function clearTimeout(id: number): void
|
|
12
15
|
function clearInterval(id: number): void
|
|
13
16
|
/** Per-frame loop, driven by the host (viewer RAF / desktop runTick). `dt` is **seconds** since
|
|
@@ -38,6 +41,9 @@ declare global {
|
|
|
38
41
|
/** Compile-time macro: `asset('./logo.svg')` yields the file as an SVG image source — for
|
|
39
42
|
* `UIImage(...)` and `bgImage`. */
|
|
40
43
|
function asset(path: `${string}.svg`): SvgSource
|
|
44
|
+
/** Compile-time macro: `asset('./levels.csv')` yields a text file's TEXT (.txt, .csv, .md, .css,
|
|
45
|
+
* .html, .xml, .yml — it ships inside the bundle). Parse it yourself. */
|
|
46
|
+
function asset(path: `${string}.${"txt" | "csv" | "md" | "css" | "html" | "xml" | "yml" | "yaml"}`): string
|
|
41
47
|
/** Compile-time macro: `asset('./hero.png')` is desugared by the bundler into the module import
|
|
42
48
|
* for that resource. The file must exist — a path that resolves to nothing fails the compile
|
|
43
49
|
* (`asset not found: ./hero.png (main.ts:3)`); there is no runtime fallback. Calls inside
|
package/src/inject.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
// ---- math ----
|
|
11
11
|
export { Mathf } from "./math/Mathf"
|
|
12
|
-
export { Vec2, Vec3 } from "./math/vec"
|
|
12
|
+
export { Vec2, Vec3, type Vec2Like, type Vec3Like } from "./math/vec"
|
|
13
13
|
export { Quat } from "./math/quat"
|
|
14
14
|
export { Mat4 } from "./math/mat4"
|
|
15
15
|
export { Color } from "./core/color"
|
package/src/runtime/app.ts
CHANGED
|
@@ -56,8 +56,10 @@ export const app = {
|
|
|
56
56
|
sdkVersion: SDK_VERSION,
|
|
57
57
|
/** The INSTALLED app: the store build this bundle runs in. After an over-the-air update the code
|
|
58
58
|
* is newer than the binary, so a feature that needs native support added in a later store build
|
|
59
|
-
* (a plugin, an SDK method) branches on `app.host.sdk` / `app.host.build` — and on
|
|
60
|
-
*
|
|
59
|
+
* (a plugin, an SDK method) branches on `app.host.sdk` / `app.host.build` — and on the feature's
|
|
60
|
+
* own check (`Push.isSupported`, `Physics.supported`), which is the final word. Not `typeof` of
|
|
61
|
+
* the global: every SDK wrapper is compiled into the bundle, so `typeof Push` is "object" on a
|
|
62
|
+
* host without the plugin. Minor and patch of the SDK never gate anything. */
|
|
61
63
|
get host(): AppHost {
|
|
62
64
|
return {
|
|
63
65
|
version: _creatorApp.appVersion?.(),
|
package/src/runtime/fetch.ts
CHANGED
|
@@ -21,6 +21,9 @@ export interface FetchResponse {
|
|
|
21
21
|
readonly status: number
|
|
22
22
|
json<T = any>(): T
|
|
23
23
|
text(): string
|
|
24
|
+
/** The body's bytes, a copy — a binary format of your own (`asset('./levels/1.bin')`), parsed in
|
|
25
|
+
* code with a DataView. Synchronous, like `text()`. */
|
|
26
|
+
bytes(): Uint8Array
|
|
24
27
|
/** @deprecated A no-op: the body is freed with the response's handle (owned handles). Apps written
|
|
25
28
|
* against the pre-owned SDK still call it after `text()` / `json()`; remove the call. */
|
|
26
29
|
dispose(): void
|
|
@@ -42,6 +45,9 @@ class FetchResponseImpl implements FetchResponse {
|
|
|
42
45
|
text(): string {
|
|
43
46
|
return _creatorFetch.toText(this._h.id)
|
|
44
47
|
}
|
|
48
|
+
bytes(): Uint8Array {
|
|
49
|
+
return _creatorFetch.toBytes(this._h.id)
|
|
50
|
+
}
|
|
45
51
|
/** @deprecated see the interface */
|
|
46
52
|
dispose(): void {}
|
|
47
53
|
}
|
package/src/runtime/storage.ts
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
// Key/value persistence backed by the host (`_creatorStorage`). Same shape as the web localStorage
|
|
2
|
-
// subset projects use.
|
|
2
|
+
// subset projects use — including `localStorage['theme'] = 'dark'` / `localStorage.theme`, the
|
|
3
|
+
// browser habit: a Proxy routes any key that is not one of the three methods to the host, so the
|
|
4
|
+
// habit saves instead of setting a field on a plain object that nothing persists.
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
export const localStorage = {
|
|
6
|
+
const methods = {
|
|
6
7
|
getItem(key: string): string | null {
|
|
7
8
|
return _creatorStorage.get(key)
|
|
8
9
|
},
|
|
9
10
|
setItem(key: string, value: string): void {
|
|
10
|
-
_creatorStorage.set(key, value)
|
|
11
|
+
_creatorStorage.set(key, String(value))
|
|
11
12
|
},
|
|
12
13
|
removeItem(key: string): void {
|
|
13
14
|
_creatorStorage.remove(key)
|
|
14
15
|
},
|
|
15
16
|
}
|
|
17
|
+
|
|
18
|
+
export type LocalStorage = typeof methods & { [key: string]: any }
|
|
19
|
+
|
|
20
|
+
export const localStorage: LocalStorage = new Proxy(methods as LocalStorage, {
|
|
21
|
+
get: (t, key) => typeof key === "string" && !(key in t) ? t.getItem(key) ?? undefined : (t as any)[key],
|
|
22
|
+
set: (t, key, value) => { if (typeof key === "string" && !(key in t)) t.setItem(key, value); return true },
|
|
23
|
+
deleteProperty: (t, key) => { if (typeof key === "string" && !(key in t)) t.removeItem(key); return true },
|
|
24
|
+
has: (t, key) => key in t || (typeof key === "string" && t.getItem(key) !== null),
|
|
25
|
+
})
|
|
@@ -173,7 +173,13 @@ export type AggregateResult<TModel, T> =
|
|
|
173
173
|
// A lazily-executed operation. `await` runs it as a single request;
|
|
174
174
|
// passing it into `db.$transaction([...])` bundles it into one atomic transaction
|
|
175
175
|
declare const __op: unique symbol
|
|
176
|
-
|
|
176
|
+
/** What a query or an op offers of a promise: `then`, `catch`, `finally` — the runtime object has
|
|
177
|
+
* all three (`op()` / the collection builder below), so `db.x.insert(…).catch(…)` type-checks. */
|
|
178
|
+
export type Settles<T> = PromiseLike<T> & {
|
|
179
|
+
catch<R = never>(onRejected?: ((reason: any) => R | PromiseLike<R>) | null): Promise<T | R>
|
|
180
|
+
finally(onFinally?: (() => void) | null): Promise<T>
|
|
181
|
+
}
|
|
182
|
+
export type Op<T> = Settles<T> & { readonly [__op]: T }
|
|
177
183
|
|
|
178
184
|
// A transport-neutral operation descriptor and the pluggable transport that runs it. The HTTP transport
|
|
179
185
|
// is selected by passing a URL string; marcidb-embedded provides an in-process FFI transport.
|
|
@@ -229,7 +235,7 @@ export type IdArg<I> = I | BareId<I>
|
|
|
229
235
|
* it to `$transaction` batches it; placing it in another query's select makes it a sub-select. `Sel` is the
|
|
230
236
|
* projection: id + scalars until `select(...)` sets a shape.
|
|
231
237
|
*/
|
|
232
|
-
export interface Query<T extends ModelTypes, Sel = T["scalars"]> extends
|
|
238
|
+
export interface Query<T extends ModelTypes, Sel = T["scalars"]> extends Settles<Rows<T, Sel>[]> {
|
|
233
239
|
readonly [__op]: Rows<T, Sel>[]
|
|
234
240
|
readonly [__sub]: T["name"]
|
|
235
241
|
readonly [__sel]: Effective<T, Sel>
|
package/src/ui/UIButton.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ClickEvent, TouchStartEvent, LongPressEvent, MouseEnterEvent } from "../runtime/touch"
|
|
2
2
|
import type { ClickEvent as ClickEventType, TouchStartEvent as TouchStartEventType, LongPressEvent as LongPressEventType, MouseEnterEvent as MouseEnterEventType } from "../runtime/touch"
|
|
3
|
-
import { type ElementStyle, type ContainerStyle, ContainerElement, buildUI, type DrawableStyle, type UIChildArg, type UIContainerBase, type UINodeChild, type ChildrenFn, type BaseStyle, type Color } from "./UINode"
|
|
3
|
+
import { type ElementStyle, type ContainerStyle, ContainerElement, buildUI, type DrawableStyle, type TextStyle, type UIChildArg, type UIContainerBase, type UINodeChild, type ChildrenFn, type BaseStyle, type Color } from "./UINode"
|
|
4
4
|
import { TREE_FLAG_CLICK, TREE_FLAG_HOVER, TREE_FLAG_INTERACTIVE, TREE_FLAG_LONG_PRESS, tree } from "./tree"
|
|
5
5
|
|
|
6
|
-
export type UIButtonStyle = ElementStyle & DrawableStyle & ContainerStyle & { rippleColor?: Color | "default" }
|
|
6
|
+
export type UIButtonStyle = ElementStyle & DrawableStyle & ContainerStyle & TextStyle & { rippleColor?: Color | "default" }
|
|
7
7
|
|
|
8
8
|
/** A tappable container — row + centered children by default. Press feedback via the `$pressed`
|
|
9
9
|
* class block or `rippleColor`. */
|
package/src/ui/UIContainer.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { ContainerElement, buildUI, type ChildrenFn, type ContainerStyle, type DrawableStyle, type ElementStyle, type UIChildArg, type UIContainerBase, type UINodeChild } from "./UINode"
|
|
1
|
+
import { ContainerElement, buildUI, type ChildrenFn, type ContainerStyle, type DrawableStyle, type ElementStyle, type TextStyle, type UIChildArg, type UIContainerBase, type UINodeChild } from "./UINode"
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/** A container's style: its box and layout, plus the text props (`color`, `fontSize`, `textWrap` …)
|
|
4
|
+
* the texts inside inherit from it. */
|
|
5
|
+
export type UIContainerStyle = ElementStyle & DrawableStyle & ContainerStyle & TextStyle
|
|
4
6
|
|
|
5
7
|
/** A vertical flex container — children stack top to bottom. */
|
|
6
8
|
export interface UIColumn extends UIContainerBase<UIContainerStyle> {
|
package/src/ui/UINode.ts
CHANGED
|
@@ -92,7 +92,7 @@ export type DrawableStyle = {
|
|
|
92
92
|
|
|
93
93
|
/** Flex-layout props of a container (how it lays out its children). */
|
|
94
94
|
export type ContainerStyle = {
|
|
95
|
-
flexDirection?: "row" | "column",
|
|
95
|
+
flexDirection?: "row" | "column" | "row-reverse" | "column-reverse",
|
|
96
96
|
justifyContent?: "flex-start" | "center" | "flex-end" | "space-between" | "space-evenly",
|
|
97
97
|
alignItems?: "flex-start" | "center" | "flex-end" | "stretch",
|
|
98
98
|
gap?: UIValue,
|
package/src/ui/UIScreen.ts
CHANGED
|
@@ -2,13 +2,13 @@ import { TouchStartEvent } from "../runtime/touch"
|
|
|
2
2
|
import { _bumpNavEpoch, _setCurrent, Presentable, type DestTuple, type DismissOptions, type PresentOptions } from "./presentable"
|
|
3
3
|
import { _transitionId, TRANSITION_NONE } from "./transitions"
|
|
4
4
|
import { createBinding, disposeBinding } from "../core/signals"
|
|
5
|
-
import { ContainerElement, acceptWidget, buildUI, type BaseStyle, type ContainerStyle, type DrawableStyle, type PaddingStyle, type UIChildArg, type UIContainerBase, type UINodeChild, type ChildrenFn, } from "./UINode"
|
|
5
|
+
import { ContainerElement, acceptWidget, buildUI, type BaseStyle, type ContainerStyle, type DrawableStyle, type PaddingStyle, type TextStyle, type UIChildArg, type UIContainerBase, type UINodeChild, type ChildrenFn, } from "./UINode"
|
|
6
6
|
import { _makeWidgetSet, type UIWidgetContent, type WidgetHost, type WidgetSet } from "./UILayer"
|
|
7
7
|
import type { UIWidget } from "./UIWidget"
|
|
8
8
|
import { TREE_DEST_SCREEN, TREE_FLAG_BACK, TREE_FLAG_INTERACTIVE, TREE_FLAG_KEEP_ALIVE, pin, tree } from "./tree"
|
|
9
9
|
import { autofocusIn } from "./autofocus"
|
|
10
10
|
|
|
11
|
-
export type UIScreenStyle = ContainerStyle & DrawableStyle & PaddingStyle & BaseStyle
|
|
11
|
+
export type UIScreenStyle = ContainerStyle & DrawableStyle & PaddingStyle & BaseStyle & TextStyle
|
|
12
12
|
|
|
13
13
|
/** A screen — the root the router/host mounts, sized to its slot. A screen NEVER scrolls: it is
|
|
14
14
|
* fixed chrome (header, tab bar) plus, when the content overflows, one `UIScrollable` body child
|
package/src/ui/UIVideo.ts
CHANGED
|
@@ -11,8 +11,9 @@ export type UIVideoStyle = ElementStyle & DrawableStyle & { objectFit?: "cover"
|
|
|
11
11
|
* fullscreen-presentable (`open()`), with seamless promotion between the two. */
|
|
12
12
|
export interface UIVideo extends UIElementBase<UIVideoStyle, DrawableStyle & BaseStyle> {
|
|
13
13
|
readonly type: "video",
|
|
14
|
-
/** The player driving this surface (playback control lives on it).
|
|
15
|
-
player
|
|
14
|
+
/** The player driving this surface (playback control lives on it). Read-only: a surface is made
|
|
15
|
+
* for its player (`UIVideo(player)`). */
|
|
16
|
+
readonly player: VideoPlayer
|
|
16
17
|
|
|
17
18
|
/** Show fullscreen as the current destination. Promotion: if this video is mounted in a screen,
|
|
18
19
|
* the SAME native player moves fullscreen — playback uninterrupted; a pop moves it back. */
|
package/src/ui/router.ts
CHANGED
|
@@ -30,7 +30,7 @@ export type PopOptions = {
|
|
|
30
30
|
* the router until `restore()`. */
|
|
31
31
|
type Router = {
|
|
32
32
|
/** The top of the router stack (not necessarily what's on screen — see `Presentable.current`). */
|
|
33
|
-
get current(): Presentable
|
|
33
|
+
get current(): Presentable | null
|
|
34
34
|
/** Mount the router with `homePage` as the stack root — the app's navigation entry point. */
|
|
35
35
|
init(homePage: Presentable, opts?: RouterOptions): void
|
|
36
36
|
/** Push a destination onto the stack — a `UIScreen`, a `Scene`, a `Scene2D`, a `NativeView`,
|
|
@@ -129,8 +129,9 @@ export const Router: Router = /*#__PURE__*/ Object.freeze({
|
|
|
129
129
|
_setCurrent(currentPage) // the stack top is visible again
|
|
130
130
|
},
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
/** The top of the stack; `null` before `Router.init` / the first push. */
|
|
133
|
+
get current(): Presentable | null {
|
|
134
|
+
return currentPage
|
|
134
135
|
},
|
|
135
136
|
|
|
136
137
|
_changeListeners: [] as any[],
|
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.2.
|
|
7
|
+
export const SDK_VERSION = "2.2.13"
|
|
8
8
|
|
|
9
9
|
/** The major of a semver string, or null when it is not one. */
|
|
10
10
|
export const sdkMajor = (version: string | null | undefined): number | null => {
|