lecodes-cli 0.7.1 → 0.8.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 +3 -3
- package/dist/index.js +2941 -745
- package/package.json +4 -4
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk/canvas/Canvas.ts +16 -0
- package/runtime/sdk/gl/Material.ts +4 -0
- package/runtime/sdk/inject.ts +8 -5
- package/runtime/sdk/plugins/service.ts +47 -0
- package/runtime/sdk/runtime/app.ts +21 -1
- package/runtime/sdk/runtime/appEvents.ts +25 -0
- package/runtime/sdk/runtime/service.ts +3 -2
- package/runtime/sdk/ui/NativeView.ts +2 -1
- package/runtime/sdk/ui/UI.ts +3 -1
- package/runtime/sdk/ui/UIButton.ts +10 -5
- package/runtime/sdk/ui/UIContainer.ts +16 -9
- package/runtime/sdk/ui/UIImage.ts +5 -2
- package/runtime/sdk/ui/UIInput.ts +75 -8
- package/runtime/sdk/ui/UIModal.ts +149 -0
- package/runtime/sdk/ui/UINode.ts +126 -55
- package/runtime/sdk/ui/UIPager.ts +297 -0
- package/runtime/sdk/ui/UIPopover.ts +106 -0
- package/runtime/sdk/ui/UIScreen.ts +11 -10
- package/runtime/sdk/ui/UIScrollable.ts +2 -1
- package/runtime/sdk/ui/UISpacer.ts +1 -1
- package/runtime/sdk/ui/UIText.ts +2 -1
- package/runtime/sdk/ui/UIVideo.ts +2 -1
- package/runtime/sdk/ui/UIVirtualizedList.ts +2 -1
- package/runtime/sdk/ui/UIWidget.ts +19 -22
- package/runtime/sdk/ui/theme.ts +66 -0
- package/runtime/sdk-types.json +1 -1
- package/runtime/sdk/compile/aspectMacro.ts +0 -42
- package/runtime/sdk/compile/assetMacro.ts +0 -45
- package/runtime/sdk/compile/bundler.ts +0 -205
- package/runtime/sdk/compile/compileProject.ts +0 -108
- package/runtime/sdk/compile/header.ts +0 -46
- package/runtime/sdk/compile/index.ts +0 -18
- package/runtime/sdk/compile/libraryImports.ts +0 -47
- package/runtime/sdk/compile/sceneEditor.ts +0 -78
- package/runtime/sdk/compile/sourcemap.ts +0 -25
- package/runtime/sdk/kit/UITabs.ts +0 -105
- package/runtime/sdk/ui/UIScreenHost.ts +0 -213
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
// Compile-time injection of an aspect's runtime name. A user aspect carries its accessor name in the
|
|
2
|
-
// (type-erased) generic — `class Health extends Aspect<'health', Sprite> {…}` — so neither the
|
|
3
|
-
// bundler nor the runtime can see it after types are stripped. This pass injects the matching
|
|
4
|
-
// `static aspect = "health"` so node.aspect()/node.health work at runtime, exactly like the SDK's
|
|
5
|
-
// built-in aspects declare it by hand.
|
|
6
|
-
//
|
|
7
|
-
// Runs on PROJECT files only (the SDK's own aspects already declare `static aspect`). Idempotent:
|
|
8
|
-
// skips a class that already declares one. String-based, like assetMacro — no parser dependency.
|
|
9
|
-
|
|
10
|
-
// `class X extends Aspect<'name', …>` up to the class body's opening brace. Group 3 = the name.
|
|
11
|
-
const ASPECT_RE = /\bclass\s+\w+\s+extends\s+Aspect\s*<\s*(["'])((?:[^"'\\]|\\.)*)\1[^{};]*>\s*\{/g
|
|
12
|
-
|
|
13
|
-
/** Cheap pre-check so we don't scan files that define no aspects. */
|
|
14
|
-
export const hasAspectExtends = (src: string): boolean => /\bextends\s+Aspect\s*</.test(src)
|
|
15
|
-
|
|
16
|
-
/** Inject `static aspect = "<name>"` into each `class … extends Aspect<'<name>', …>`. */
|
|
17
|
-
export const injectAspectNames = (src: string): string => {
|
|
18
|
-
let out = ""
|
|
19
|
-
let last = 0
|
|
20
|
-
ASPECT_RE.lastIndex = 0
|
|
21
|
-
let m: RegExpExecArray | null
|
|
22
|
-
while ((m = ASPECT_RE.exec(src)) !== null) {
|
|
23
|
-
const braceEnd = m.index + m[0].length // index just past the opening `{`
|
|
24
|
-
if (!classBodyDeclaresAspect(src, braceEnd)) {
|
|
25
|
-
out += src.slice(last, braceEnd) + ` static aspect = ${JSON.stringify(m[2])};`
|
|
26
|
-
last = braceEnd
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return last === 0 ? src : out + src.slice(last)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Does the class body starting just after `from` (its `{`) already declare a `static aspect`? */
|
|
33
|
-
const classBodyDeclaresAspect = (src: string, from: number): boolean => {
|
|
34
|
-
let depth = 1
|
|
35
|
-
let i = from
|
|
36
|
-
for (; i < src.length && depth > 0; i++) {
|
|
37
|
-
const c = src[i]
|
|
38
|
-
if (c === "{") depth++
|
|
39
|
-
else if (c === "}") depth--
|
|
40
|
-
}
|
|
41
|
-
return /\bstatic\b[^;{}()=]*\baspect\b/.test(src.slice(from, i))
|
|
42
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
// Ported from packages/lecodes-cli/src/compile/assetMacro.ts (itself from the backend). The
|
|
2
|
-
// compile-time `asset('./x.png')` macro is desugared into a real import so it goes through the same
|
|
3
|
-
// loader/resolver as a hand-written import.
|
|
4
|
-
|
|
5
|
-
// Matches `asset("./img.png")` / `asset('./img.png')` calls whose argument is a single string
|
|
6
|
-
// literal (handling escaped chars inside the literal). The lookbehind excludes property access —
|
|
7
|
-
// a METHOD named asset (`ui.asset("model")`, InspectorUI) is not the macro.
|
|
8
|
-
const ASSET_RE = /(?<![.\w$])asset\(\s*(["'])((?:[^"'\\]|\\.)*)\1\s*\)/g
|
|
9
|
-
|
|
10
|
-
/** Cheap pre-check so we don't run the regex over every file. */
|
|
11
|
-
export const hasAssetMacro = (src: string): boolean => src.includes("asset(")
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Desugars `asset('./path.png')` into the exact import the user could have written by hand.
|
|
15
|
-
* Identical paths within a file map to one identifier. Only string-literal arguments are supported.
|
|
16
|
-
*/
|
|
17
|
-
export const desugarAssetMacros = (src: string, filePath: string): string => {
|
|
18
|
-
const byPath = new Map<string, string>()
|
|
19
|
-
let i = 0
|
|
20
|
-
|
|
21
|
-
const out = src.replace(ASSET_RE, (_match, _quote, path) => {
|
|
22
|
-
let name = byPath.get(path)
|
|
23
|
-
if (!name) {
|
|
24
|
-
name = `__asset_${i++}`
|
|
25
|
-
byPath.set(path, name)
|
|
26
|
-
}
|
|
27
|
-
return name
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
if (/(?<![.\w$])asset\(/.test(out)) {
|
|
31
|
-
throw new Error(`asset() expects a string literal, found a dynamic argument in: ${filePath}`)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (byPath.size === 0) return src
|
|
35
|
-
|
|
36
|
-
const imports = [ ...byPath ]
|
|
37
|
-
.map(([ path, name ]) => `import ${name} from ${JSON.stringify(path)};`)
|
|
38
|
-
.join("\n")
|
|
39
|
-
|
|
40
|
-
// APPENDED, never prepended. ESM imports hoist, so their position is semantically irrelevant —
|
|
41
|
-
// but lines added at the TOP would shift every original line, and the bundle's source map would
|
|
42
|
-
// then disagree with the file the editor shows: inspector jumps and error-overlay stack frames
|
|
43
|
-
// land N lines off in exactly (and only) the files that use asset(). Appending adds no offsets.
|
|
44
|
-
return out + "\n" + imports
|
|
45
|
-
}
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"
|
|
2
|
-
import { spawnSync } from "node:child_process"
|
|
3
|
-
import { createRequire } from "node:module"
|
|
4
|
-
import { dirname, join, relative } from "node:path"
|
|
5
|
-
import { fileURLToPath } from "node:url"
|
|
6
|
-
import { desugarAssetMacros, hasAssetMacro } from "./assetMacro"
|
|
7
|
-
import { hasAspectExtends, injectAspectNames } from "./aspectMacro"
|
|
8
|
-
import { hasImportStatement, rewriteLibraryImports } from "./libraryImports"
|
|
9
|
-
|
|
10
|
-
/*
|
|
11
|
-
* The user-project bundler. Project files live in an in-memory map; the whole SDK is delivered via
|
|
12
|
-
* chisel's `inject` so user code uses globals (Scene, Sprite, toast, …) with NO imports. chisel — the
|
|
13
|
-
* Rust method-granular bundler (@letary/chisel) — tree-shakes the injected SDK down to what the
|
|
14
|
-
* project references, including unused class *methods*. It is the only bundler (the esbuild fallback
|
|
15
|
-
* is gone). Bare imports are a hard error (chisel's resolver enforces it).
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
export type MemoryFiles = Record<string, string>
|
|
19
|
-
|
|
20
|
-
/** Locate the SDK inject entry: an explicit override (LECODES_SDK_INJECT — used by the published CLI,
|
|
21
|
-
* which vendors the SDK source), else the source in-repo. chisel needs the SOURCE tree (not a bundle). */
|
|
22
|
-
const resolveInject = (): string => {
|
|
23
|
-
const override = process.env.LECODES_SDK_INJECT
|
|
24
|
-
if (override && existsSync(override)) return override
|
|
25
|
-
const here = dirname(fileURLToPath(import.meta.url)) // …/sdk/src/compile — portable (Node + Bun)
|
|
26
|
-
const src = join(here, "..", "inject.ts")
|
|
27
|
-
if (existsSync(src)) return src
|
|
28
|
-
throw new Error("sdk: could not locate the inject source (LECODES_SDK_INJECT / src/inject.ts)")
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const normalize = (path: string): string => {
|
|
32
|
-
const parts: string[] = []
|
|
33
|
-
for (const part of path.split("/")) {
|
|
34
|
-
if (!part || part === ".") continue
|
|
35
|
-
if (part === "..") parts.pop()
|
|
36
|
-
else parts.push(part)
|
|
37
|
-
}
|
|
38
|
-
return "/" + parts.join("/")
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ---- chisel (Rust) bundler -------------------------------------------------------------------
|
|
42
|
-
//
|
|
43
|
-
// chisel is fed an in-memory file map over JSON-stdio: the SDK source under `/__sdk/…` (injected via
|
|
44
|
-
// `/__sdk/inject.ts`), the project files at their own paths, and a `define` map. It does
|
|
45
|
-
// method-granular tree-shaking + (when minify is on) compress + mangle. Assets/shaders/svg reach it
|
|
46
|
-
// as `asset()` macros desugared to default imports; aspect names are injected as `static aspect`.
|
|
47
|
-
|
|
48
|
-
// `EDITOR` is a boolean define: `false` (const-folded, `if (EDITOR)` branches DCE'd away) in
|
|
49
|
-
// every shipped bundle, `true` only in scene-editor compiles (CompileOptions.editor). Requires a
|
|
50
|
-
// chisel with boolean-define support — an older binary ignores the entry and leaves `EDITOR` as a
|
|
51
|
-
// free identifier (a runtime ReferenceError only in code that actually uses it).
|
|
52
|
-
const CHISEL_DEFINE: Record<string, string> = { RAD2DEG: "57.29577951308232", DEG2RAD: "0.017453292519943295" }
|
|
53
|
-
|
|
54
|
-
// Methods the native host invokes by name have no in-bundle caller, so method-DCE can't see them.
|
|
55
|
-
// The SDK prefixes its framework / host-facing methods with `_` (`_emitTouchStart`, `_emitClick`,
|
|
56
|
-
// `_onAnimationComplete`, `_emitAnim`, …), so keep every underscore-prefixed method on classes that survive.
|
|
57
|
-
const CHISEL_KEEP = [ "_*" ]
|
|
58
|
-
|
|
59
|
-
/** The SDK source root (parent of `inject.ts`). chisel needs the source tree, not a prebuilt bundle. */
|
|
60
|
-
const resolveSdkSrc = (): string => {
|
|
61
|
-
const inject = resolveInject()
|
|
62
|
-
if (!inject.endsWith("inject.ts")) {
|
|
63
|
-
throw new Error(`chisel needs the SDK source tree (src/inject.ts); got ${inject}. Set LECODES_SDK_INJECT to the source.`)
|
|
64
|
-
}
|
|
65
|
-
return dirname(inject)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const isSdkSource = (path: string) =>
|
|
69
|
-
/\.[tj]s$/.test(path) && !path.endsWith(".d.ts") &&
|
|
70
|
-
!path.includes("/compile/") && !path.includes("/tests/") && !path.includes("/__tests__/")
|
|
71
|
-
|
|
72
|
-
// The SDK tree doesn't change within a process — read it once.
|
|
73
|
-
const sdkFilesCache = new Map<string, MemoryFiles>()
|
|
74
|
-
const collectSdkFiles = (sdkSrc: string): MemoryFiles => {
|
|
75
|
-
const cached = sdkFilesCache.get(sdkSrc)
|
|
76
|
-
if (cached) return cached
|
|
77
|
-
const out: MemoryFiles = {}
|
|
78
|
-
const walk = (dir: string) => {
|
|
79
|
-
for (const name of readdirSync(dir)) {
|
|
80
|
-
const p = join(dir, name)
|
|
81
|
-
if (statSync(p).isDirectory()) walk(p)
|
|
82
|
-
else if (isSdkSource(p)) out["/__sdk/" + relative(sdkSrc, p).split("\\").join("/")] = readFileSync(p, "utf8")
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
walk(sdkSrc)
|
|
86
|
-
sdkFilesCache.set(sdkSrc, out)
|
|
87
|
-
return out
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Locate the chisel binary: an explicit override (LECODES_CHISEL_BIN), else the prebuilt binary from
|
|
91
|
-
* the @letary/chisel npm package (which resolves the platform via optional @letary/chisel-<platform>). */
|
|
92
|
-
const resolveChiselBin = (): string => {
|
|
93
|
-
const override = process.env.LECODES_CHISEL_BIN
|
|
94
|
-
if (override) {
|
|
95
|
-
if (existsSync(override)) return override
|
|
96
|
-
throw new Error(`LECODES_CHISEL_BIN points at a missing file: ${override}`)
|
|
97
|
-
}
|
|
98
|
-
try {
|
|
99
|
-
const bin = createRequire(import.meta.url)("@letary/chisel").binaryPath() as string | null
|
|
100
|
-
if (bin && existsSync(bin)) return bin
|
|
101
|
-
} catch { /* @letary/chisel (or its platform binary) not installed */ }
|
|
102
|
-
throw new Error("chisel binary not found. Install @letary/chisel, or set LECODES_CHISEL_BIN.")
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const bundleProjectChisel = async (
|
|
106
|
-
entrypoint: string,
|
|
107
|
-
files: MemoryFiles,
|
|
108
|
-
format: "esm" | "iife",
|
|
109
|
-
sourcemap: boolean,
|
|
110
|
-
minifyOverride?: boolean,
|
|
111
|
-
editor?: boolean,
|
|
112
|
-
libraries?: Record<string, MemoryFiles>,
|
|
113
|
-
): Promise<{ code: string, map: string | null }> => {
|
|
114
|
-
const entry = normalize(entrypoint)
|
|
115
|
-
const slugs = new Set(Object.keys(libraries ?? {}))
|
|
116
|
-
// Per-file source transforms (project files only — SDK aspects declare their own `static aspect`):
|
|
117
|
-
// - rewrite bare library imports (`from "acme-ui"`) → the injected `/__lib/<slug>/…` tree
|
|
118
|
-
// - desugar `asset('./x')` → a default import that resolves to the synthetic asset module
|
|
119
|
-
// - inject `static aspect = '<name>'` from `class … extends Aspect<'<name>', …>`
|
|
120
|
-
const projFiles: MemoryFiles = {}
|
|
121
|
-
for (const [ k, v ] of Object.entries(files)) {
|
|
122
|
-
const path = normalize(k)
|
|
123
|
-
let src = v
|
|
124
|
-
if (/\.[tj]sx?$/.test(path)) {
|
|
125
|
-
if (hasImportStatement(src)) src = rewriteLibraryImports(src, slugs)
|
|
126
|
-
if (hasAssetMacro(src)) src = desugarAssetMacros(src, path)
|
|
127
|
-
if (hasAspectExtends(src)) src = injectAspectNames(src)
|
|
128
|
-
}
|
|
129
|
-
projFiles[path] = src
|
|
130
|
-
}
|
|
131
|
-
if (!projFiles[entry]) throw new Error(`Entrypoint not found: ${entry}`)
|
|
132
|
-
|
|
133
|
-
// Library dependencies: merge each library's files under /__lib/<slug>/… (like /__sdk/…). They
|
|
134
|
-
// get the same asset/aspect desugaring as project code, but NOT the bare-import rewrite — a
|
|
135
|
-
// library can't declare dependencies (flat graph v1), so any bare import in a library is an
|
|
136
|
-
// error chisel surfaces. Library-internal relative imports resolve within the prefix untouched.
|
|
137
|
-
for (const [ slug, libFiles ] of Object.entries(libraries ?? {})) {
|
|
138
|
-
for (const [ k, v ] of Object.entries(libFiles)) {
|
|
139
|
-
const path = normalize(`/__lib/${slug}/${k}`)
|
|
140
|
-
let src = v
|
|
141
|
-
if (/\.[tj]sx?$/.test(path)) {
|
|
142
|
-
if (hasAssetMacro(src)) src = desugarAssetMacros(src, path)
|
|
143
|
-
if (hasAspectExtends(src)) src = injectAspectNames(src)
|
|
144
|
-
}
|
|
145
|
-
projFiles[path] = src
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const sdkFiles = collectSdkFiles(resolveSdkSrc())
|
|
150
|
-
// Shipped bundles minify by default; edit-mode bundles pass `false` — mangling erases class
|
|
151
|
-
// names, which the scene editor matches aspect schemas by (`describeAspect().className`).
|
|
152
|
-
const minify = minifyOverride ?? (process.env.LECODES_CHISEL_MINIFY !== "0")
|
|
153
|
-
const input = JSON.stringify({
|
|
154
|
-
files: { ...sdkFiles, ...projFiles },
|
|
155
|
-
entry,
|
|
156
|
-
inject: [ "/__sdk/inject.ts" ],
|
|
157
|
-
format,
|
|
158
|
-
minify,
|
|
159
|
-
fuse: process.env.LECODES_CHISEL_FUSE === "1",
|
|
160
|
-
define: { ...CHISEL_DEFINE, EDITOR: editor ? "true" : "false" },
|
|
161
|
-
keep: CHISEL_KEEP,
|
|
162
|
-
sourcemap,
|
|
163
|
-
// Reactive-UI desugaring (memoized children .map → __uiMap + auto-wrapped signal reads in
|
|
164
|
-
// UIText/style args). Only fires on free references to the injected globals, i.e. user code.
|
|
165
|
-
// An older chisel binary ignores the unknown field — bundles still work, children just
|
|
166
|
-
// rebuild per change instead of reusing nodes.
|
|
167
|
-
reactive_ui: process.env.LECODES_CHISEL_REACTIVE_UI !== "0",
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
const bin = resolveChiselBin()
|
|
171
|
-
const r = spawnSync(bin, [], { input, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 })
|
|
172
|
-
if (r.status === 2 || r.error) {
|
|
173
|
-
throw new Error(`chisel failed: ${r.error?.message ?? r.stderr ?? "unknown error"}`)
|
|
174
|
-
}
|
|
175
|
-
let parsed: { code?: string, map?: string, error?: string }
|
|
176
|
-
try {
|
|
177
|
-
parsed = JSON.parse(r.stdout || "{}")
|
|
178
|
-
} catch {
|
|
179
|
-
throw new Error(`chisel produced invalid output: ${(r.stdout || r.stderr || "").slice(0, 200)}`)
|
|
180
|
-
}
|
|
181
|
-
if (parsed.error) throw new Error(`chisel: ${parsed.error}`)
|
|
182
|
-
if (parsed.code == null) throw new Error("chisel produced no code")
|
|
183
|
-
return { code: parsed.code, map: parsed.map ?? null }
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Bundle the project `files` (path → source) starting at `entrypoint`, with the SDK injected.
|
|
188
|
-
* `format` is "esm" by default (what the platform ships); "iife" produces a self-running script.
|
|
189
|
-
*/
|
|
190
|
-
export const bundleProject = async (
|
|
191
|
-
entrypoint: string,
|
|
192
|
-
files: MemoryFiles,
|
|
193
|
-
format: "esm" | "iife" = "esm",
|
|
194
|
-
): Promise<string> => (await bundleProjectChisel(entrypoint, files, format, false)).code
|
|
195
|
-
|
|
196
|
-
/** Like `bundleProject`, but also returns chisel's source map. The map's `sources` are the original
|
|
197
|
-
* module paths and its generated positions are relative to `code` (line 1 = first line of `code`). */
|
|
198
|
-
export const bundleProjectWithMap = async (
|
|
199
|
-
entrypoint: string,
|
|
200
|
-
files: MemoryFiles,
|
|
201
|
-
format: "esm" | "iife" = "esm",
|
|
202
|
-
minify?: boolean,
|
|
203
|
-
editor?: boolean,
|
|
204
|
-
libraries?: Record<string, MemoryFiles>,
|
|
205
|
-
): Promise<{ code: string, map: string | null }> => bundleProjectChisel(entrypoint, files, format, true, minify, editor, libraries)
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
// Turns a project's files into the esbuild input map + entrypoint + metadata header, then bundles.
|
|
2
|
-
// The one canonical "user project → deployable bundle" path: the backend and CLI both call this
|
|
3
|
-
// (the backend resolves asset URLs from the DB, the CLI from local metadata — same function, the
|
|
4
|
-
// URLs are passed in as `publicUrl` + per-entry `fileSrc`/`shaders`).
|
|
5
|
-
|
|
6
|
-
import { bundleProjectWithMap, type MemoryFiles } from "./bundler"
|
|
7
|
-
import { buildHeader } from "./header"
|
|
8
|
-
import { inlineSourceMapComment, offsetSourceMap } from "./sourcemap"
|
|
9
|
-
|
|
10
|
-
export type CompileFileType = "text" | "resource" | "shader"
|
|
11
|
-
|
|
12
|
-
export type CompileEntry = {
|
|
13
|
-
path: string
|
|
14
|
-
type: CompileFileType
|
|
15
|
-
/** Source text for code / .svg files. */
|
|
16
|
-
text?: string
|
|
17
|
-
/** Server src for a binary resource (e.g. "/uploads/x.png"); prefixed with publicUrl. */
|
|
18
|
-
fileSrc?: string
|
|
19
|
-
/** Compiled-shader artifacts (platform + src); the first drives the per-backend URL. */
|
|
20
|
-
shaders?: { platform: string, src: string }[]
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export type CompileOptions = {
|
|
24
|
-
entries: CompileEntry[]
|
|
25
|
-
name: string
|
|
26
|
-
publicUrl: string
|
|
27
|
-
entryOverride?: string
|
|
28
|
-
/** Prepend the metadata header (default true). */
|
|
29
|
-
header?: boolean
|
|
30
|
-
/** Generated-icon URL for the header (backend only). */
|
|
31
|
-
icon?: string
|
|
32
|
-
/** "esm" (default, shipped) or "iife" (self-running, for the local renderer). */
|
|
33
|
-
format?: "esm" | "iife"
|
|
34
|
-
/** Resolve `asset()` resources to a LOCAL host asset at runtime instead of a `publicUrl` URL: each
|
|
35
|
-
* resource compiles to `"id:" + _creatorUtils.fetchLocal(<name>)` (the host reads the file by name
|
|
36
|
-
* and returns its systemId). For hosts with no asset server (the desktop native host), where a URL
|
|
37
|
-
* can't be fetched. Off by default — the shipped/web build keeps the URL form. */
|
|
38
|
-
localAssets?: boolean
|
|
39
|
-
/** Override minification (default: on, `LECODES_CHISEL_MINIFY` env aside). Scene-editor bundles
|
|
40
|
-
* compile with `false`: mangling erases class names, which the editor matches aspect schemas by. */
|
|
41
|
-
minify?: boolean
|
|
42
|
-
/** Editor compile: the `EDITOR` global compiles to `true` (scene-editor bundles). Default false —
|
|
43
|
-
* shipped bundles get `EDITOR` = `false`, const-folded so `if (EDITOR)` branches are DCE'd. */
|
|
44
|
-
editor?: boolean
|
|
45
|
-
/** Component-library dependencies: slug → the library's files (root-relative paths). Merged under
|
|
46
|
-
* `/__lib/<slug>/…` and reachable from project code via `import … from "<slug>"`. See
|
|
47
|
-
* docs/library-system-plan.md §4. */
|
|
48
|
-
libraries?: Record<string, MemoryFiles>
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** The default entrypoint: the largest top-level .ts/.js file with no `export` (a script entry). */
|
|
52
|
-
const detectEntry = (entries: CompileEntry[]): string => {
|
|
53
|
-
let entry = ""
|
|
54
|
-
let max = -1
|
|
55
|
-
for (const e of entries) {
|
|
56
|
-
const ext = e.path.split(".").at(-1)?.toLowerCase()
|
|
57
|
-
// `.editor.ts` files are editor-only plugin modules — never a production entrypoint.
|
|
58
|
-
if (e.path.endsWith(".editor.ts")) continue
|
|
59
|
-
if (e.type === "text" && e.text !== undefined && !e.text.includes("export") && (ext === "ts" || ext === "js")) {
|
|
60
|
-
if (e.text.length > max) { entry = e.path; max = e.text.length }
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return entry
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export const compileProject = async (opts: CompileOptions): Promise<string> => {
|
|
67
|
-
const entry = opts.entryOverride ?? detectEntry(opts.entries)
|
|
68
|
-
if (!entry) {
|
|
69
|
-
throw new Error("Couldn't find an entrypoint (a top-level .ts/.js file with no `export`). Pass entryOverride.")
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const files: Record<string, string> = {}
|
|
73
|
-
for (const e of opts.entries) {
|
|
74
|
-
if (e.type === "resource") {
|
|
75
|
-
if (opts.localAssets) {
|
|
76
|
-
// The host resolves the asset by name at runtime (reads the local file → systemId); the
|
|
77
|
-
// "id:" prefix is the host's local-resource URL scheme (parsed by the UI + 2D texture paths).
|
|
78
|
-
const name = e.path.split(/[\\/]/).pop() ?? e.path
|
|
79
|
-
files[e.path] = `export default ("id:" + _creatorUtils.fetchLocal(${JSON.stringify(name)}))`
|
|
80
|
-
} else {
|
|
81
|
-
files[e.path] = `export default "${opts.publicUrl}${e.fileSrc ?? ""}"`
|
|
82
|
-
}
|
|
83
|
-
} else if (e.type === "shader") {
|
|
84
|
-
const shaders = e.shaders ?? []
|
|
85
|
-
if (shaders.length > 2) {
|
|
86
|
-
files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.subBackend ?? _creator.backend}")}\``
|
|
87
|
-
} else if (shaders.length > 0) {
|
|
88
|
-
files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.backend}")}\``
|
|
89
|
-
} else {
|
|
90
|
-
files[e.path] = `export default ""` // local-only shader with no compiled artifact yet
|
|
91
|
-
}
|
|
92
|
-
} else if (e.path.toLowerCase().endsWith(".svg")) {
|
|
93
|
-
files[e.path] = `export default SvgSource(\`${e.text ?? ""}\`)`
|
|
94
|
-
} else {
|
|
95
|
-
files[e.path] = e.text ?? ""
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const { code, map } = await bundleProjectWithMap(entry, files, opts.format ?? "esm", opts.minify, opts.editor, opts.libraries)
|
|
100
|
-
if (opts.header === false) {
|
|
101
|
-
return map ? `${code}\n${inlineSourceMapComment(map)}` : code
|
|
102
|
-
}
|
|
103
|
-
const header = buildHeader(opts.name, code, { icon: opts.icon })
|
|
104
|
-
if (!map) return header + code
|
|
105
|
-
// Shift the map down past the header (prepended before the code); the inline comment lands last.
|
|
106
|
-
const headerLines = (header.match(/\n/g) ?? []).length
|
|
107
|
-
return `${header}${code}\n${inlineSourceMapComment(offsetSourceMap(map, headerLines))}`
|
|
108
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
// The metadata header the launcher reads from the compiled bundle: name / icon / preload / gl / 2d /
|
|
2
|
-
// ar. Single source of truth — the backend and CLI both call this (today they mirror it by hand).
|
|
3
|
-
//
|
|
4
|
-
// Detection keys on the *bundled* `_creator*` calls, which the SDK's thin wrappers still emit
|
|
5
|
-
// (new Scene() → _creator.createScene, new Scene2D() → _creator2d.createScene), so the re-skin keeps
|
|
6
|
-
// the exact same signals the old worker bundles produced.
|
|
7
|
-
|
|
8
|
-
export type HeaderOptions = {
|
|
9
|
-
/** Generated-icon URL (backend supplies it; the CLI omits it). */
|
|
10
|
-
icon?: string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export const buildHeader = (name: string, code: string, opts: HeaderOptions = {}): string => {
|
|
14
|
-
let header = `// name: ${name.slice(0, 20)}\n`
|
|
15
|
-
if (opts.icon) header += `// icon: ${opts.icon}\n`
|
|
16
|
-
header += `// last-updated: ${new Date().toISOString()}\n`
|
|
17
|
-
|
|
18
|
-
// Local assets to preload: explicit fetchLocal() calls + implicit engine archives.
|
|
19
|
-
const localAssets = new Set<string>()
|
|
20
|
-
for (const m of code.matchAll(/_creatorUtils\.fetchLocal\((.+?)\)/g)) {
|
|
21
|
-
localAssets.add(m[1].replaceAll(/['"]/g, "").trim())
|
|
22
|
-
}
|
|
23
|
-
if (/_creator\.createGlb/.test(code)) localAssets.add("uberarchive")
|
|
24
|
-
if (/_creator\.captureImage/.test(code)) localAssets.add("capture")
|
|
25
|
-
if (localAssets.size > 0) header += `// preload: ${[ ...localAssets ].join(";")}\n`
|
|
26
|
-
|
|
27
|
-
// GL: disabled unless the project touches the 3D engine; deferred if it lazily creates the engine.
|
|
28
|
-
if (!code.includes("_creator.createScene") && !code.includes("_creator.createEntity")) header += `// gl: disable\n`
|
|
29
|
-
else if (code.includes("_creator.createEngine")) header += `// gl: defer\n`
|
|
30
|
-
|
|
31
|
-
// 2D (creator-2d) — non-exclusive: emitted alongside `gl: disable` so hosts skip Filament and a
|
|
32
|
-
// 2D-aware host brings up the lightweight sokol engine instead.
|
|
33
|
-
if (code.includes("_creator2d.")) header += `// 2d: enable\n`
|
|
34
|
-
|
|
35
|
-
if (code.includes("_creator.createARController")) header += "// ar: enable"
|
|
36
|
-
header += "\n"
|
|
37
|
-
return header
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Same engine signals the header uses, for callers that need the project kind (e.g. icon type). */
|
|
41
|
-
export const detectProjectKind = (code: string): "ar" | "ui" | "2d" | "3d" => {
|
|
42
|
-
if (code.includes("_creator.createARController")) return "ar"
|
|
43
|
-
if (code.includes("_creator2d.")) return "2d"
|
|
44
|
-
if (!code.includes("_creator.")) return "ui"
|
|
45
|
-
return "3d"
|
|
46
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// The public entry of the SDK's bundle build system. The backend, CLI, and local renderer import
|
|
2
|
-
// from here instead of re-forking a bundler. (Node + Bun safe — no top-level host globals.)
|
|
3
|
-
|
|
4
|
-
export { bundleProject, bundleProjectWithMap, type MemoryFiles } from "./bundler"
|
|
5
|
-
export { offsetSourceMap, inlineSourceMapComment } from "./sourcemap"
|
|
6
|
-
export { buildHeader, detectProjectKind, type HeaderOptions } from "./header"
|
|
7
|
-
export {
|
|
8
|
-
compileProject,
|
|
9
|
-
type CompileEntry,
|
|
10
|
-
type CompileOptions,
|
|
11
|
-
type CompileFileType,
|
|
12
|
-
} from "./compileProject"
|
|
13
|
-
export {
|
|
14
|
-
loadSceneHarness,
|
|
15
|
-
sceneEditorEntries,
|
|
16
|
-
SCENE_ENTRY_PATH,
|
|
17
|
-
SCENE_HARNESS_DIR,
|
|
18
|
-
} from "./sceneEditor"
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
// Pre-chisel rewrite of user-library bare-specifier imports. chisel's resolver hard-errors on any
|
|
2
|
-
// bare import (the whole SDK is globals), so a project that imports a component library by its slug
|
|
3
|
-
// —`import { Card } from "acme-ui"`— is rewritten here to the injected library tree under
|
|
4
|
-
// `/__lib/<slug>/…` BEFORE chisel runs. Only libraries present in the dependency map are known
|
|
5
|
-
// slugs; any other bare specifier is an author error, surfaced with a friendly message instead of
|
|
6
|
-
// chisel's generic one. Static string-literal specifiers only — dynamic `import()` is unsupported
|
|
7
|
-
// (documented). Chisel itself is untouched. See docs/library-system-plan.md §4.
|
|
8
|
-
|
|
9
|
-
/** Cheap pre-check so the regexes don't run over files with no imports at all. */
|
|
10
|
-
export const hasImportStatement = (src: string): boolean => src.includes("import") || src.includes("from")
|
|
11
|
-
|
|
12
|
-
// A static `from "…"` clause (import-from OR export-from). Anchored at a statement boundary
|
|
13
|
-
// (line start / `;` / `}`) and disallowing quotes/backticks/semicolons between the keyword and
|
|
14
|
-
// `from`, so it can never span into a string literal or a neighbouring statement. Named-import
|
|
15
|
-
// lists may span multiple lines (newlines are allowed between the keyword and `from`).
|
|
16
|
-
const IMPORT_FROM_RE = /(^|[;\n}])(\s*(?:import|export)\b[^"'`;]*?\bfrom\s*)(["'])([^"'\n]+)\3/g
|
|
17
|
-
|
|
18
|
-
// A side-effect `import "…"` (a bare quote directly after the keyword — not `import x`, not `import(`).
|
|
19
|
-
const SIDE_EFFECT_RE = /(^|[;\n}])(\s*import\s*)(["'])([^"'\n]+)\3/g
|
|
20
|
-
|
|
21
|
-
const isRelativeOrAbsolute = (spec: string): boolean =>
|
|
22
|
-
spec.startsWith("/") || spec.startsWith("./") || spec.startsWith("../")
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Rewrite every known-library bare import in `src` to its `/__lib/<slug>/…` path. Relative and
|
|
26
|
-
* absolute specifiers pass through untouched; an unknown bare specifier throws a friendly Error.
|
|
27
|
-
* `slugs` is the set of library slugs available to this project (its dependency map's keys).
|
|
28
|
-
*/
|
|
29
|
-
export const rewriteLibraryImports = (src: string, slugs: Set<string>): string => {
|
|
30
|
-
const rewriteSpec = (spec: string): string => {
|
|
31
|
-
if (isRelativeOrAbsolute(spec)) return spec
|
|
32
|
-
const slash = spec.indexOf("/")
|
|
33
|
-
const slug = slash === -1 ? spec : spec.slice(0, slash)
|
|
34
|
-
if (slugs.has(slug)) {
|
|
35
|
-
// `acme-ui` → `/__lib/acme-ui/index`; `acme-ui/theme` → `/__lib/acme-ui/theme`.
|
|
36
|
-
const sub = slash === -1 ? "/index" : spec.slice(slash)
|
|
37
|
-
return `/__lib/${slug}${sub}`
|
|
38
|
-
}
|
|
39
|
-
throw new Error(`Unknown library "${slug}" — add it in Settings → Libraries, or use a relative path like "./${slug}".`)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
let out = src.replace(IMPORT_FROM_RE, (_m, boundary, head, quote, spec) =>
|
|
43
|
-
`${boundary}${head}${quote}${rewriteSpec(spec)}${quote}`)
|
|
44
|
-
out = out.replace(SIDE_EFFECT_RE, (m, boundary, head, quote, spec) =>
|
|
45
|
-
isRelativeOrAbsolute(spec) ? m : `${boundary}${head}${quote}${rewriteSpec(spec)}${quote}`)
|
|
46
|
-
return out
|
|
47
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
// The scene-editor compile composition, shared by every host that compiles an edit-mode bundle
|
|
2
|
-
// for a .scene.ts file: the platform backend (src/scene/compileSceneEditor.ts), the standalone
|
|
3
|
-
// scene-editor dev server, and (later) the lecodes CLI. One bundle on purpose: chisel's
|
|
4
|
-
// method-granular DCE trims each bundle's SDK copy to its own calls, so a separately-compiled
|
|
5
|
-
// harness could not call scene methods on objects the scene bundle created.
|
|
6
|
-
|
|
7
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs"
|
|
8
|
-
import { join } from "node:path"
|
|
9
|
-
import type { CompileEntry } from "./compileProject"
|
|
10
|
-
|
|
11
|
-
/** Where the harness sources land in the compile's file map. The dotfolder can't collide with
|
|
12
|
-
* project assets (the tree forbids leading-dot names) and reads clearly in compile errors. */
|
|
13
|
-
export const SCENE_HARNESS_DIR = "./.scene-editor"
|
|
14
|
-
|
|
15
|
-
/** The virtual entrypoint path (`entryOverride`). */
|
|
16
|
-
export const SCENE_ENTRY_PATH = "./.scene-entry.ts"
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Read the harness sources (packages/projects/src/scene-editor) from a directory:
|
|
20
|
-
* `LECODES_SCENE_HARNESS_DIR` env override first (deployments vendor the sources, like
|
|
21
|
-
* LECODES_SDK_INJECT does for the SDK), else the given fallback dir. Returns name → source.
|
|
22
|
-
*/
|
|
23
|
-
export const loadSceneHarness = (fallbackDir?: string): Record<string, string> => {
|
|
24
|
-
const override = process.env.LECODES_SCENE_HARNESS_DIR
|
|
25
|
-
const dir = override && existsSync(override) ? override
|
|
26
|
-
: fallbackDir && existsSync(fallbackDir) ? fallbackDir
|
|
27
|
-
: null
|
|
28
|
-
if (!dir) throw new Error("scene-editor harness sources not found (set LECODES_SCENE_HARNESS_DIR)")
|
|
29
|
-
const out: Record<string, string> = {}
|
|
30
|
-
for (const f of readdirSync(dir)) {
|
|
31
|
-
if (f.endsWith(".ts")) out[f] = readFileSync(join(dir, f), "utf8")
|
|
32
|
-
}
|
|
33
|
-
return out
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* The extra entries that turn a project compile into a scene-editor compile: the harness modules
|
|
38
|
-
* plus a virtual entry that (1) sets the edit-mode flag in its own module — imports hoist, so the
|
|
39
|
-
* flag must run as a module, first — (2) imports the scene, which registers its handle on
|
|
40
|
-
* `__lecodesScenes`, (3) imports the project's `*.editor.ts` plugin files (editor bundles ONLY —
|
|
41
|
-
* production compiles never import them, so they cost zero bytes in shipped `app.js`), and
|
|
42
|
-
* (4) imports the harness main, which installs `__lecodesSceneHarness`.
|
|
43
|
-
* FULL extensions everywhere — exact-match specifiers need no resolver guesswork on dotted names,
|
|
44
|
-
* so even the unpatched published chisel resolves the `.scene.ts` import.
|
|
45
|
-
*
|
|
46
|
-
* `scenePath` is the project path of the scene file ("./scenes/city.scene.ts"); `projectPaths` is
|
|
47
|
-
* the project's file list — `.editor.ts` entries are picked out of it. Compile with
|
|
48
|
-
* `entryOverride: SCENE_ENTRY_PATH`, `header: false`, `format: "iife"`, `editor: true`.
|
|
49
|
-
*/
|
|
50
|
-
export const sceneEditorEntries = (
|
|
51
|
-
scenePath: string,
|
|
52
|
-
harness: Record<string, string>,
|
|
53
|
-
projectPaths: string[] = [],
|
|
54
|
-
): CompileEntry[] => {
|
|
55
|
-
const entries: CompileEntry[] = Object.entries(harness).map(([ name, text ]) => ({
|
|
56
|
-
path: `${SCENE_HARNESS_DIR}/${name}`,
|
|
57
|
-
type: "text" as const,
|
|
58
|
-
text,
|
|
59
|
-
}))
|
|
60
|
-
entries.push({
|
|
61
|
-
path: `${SCENE_HARNESS_DIR}/.edit-mode.ts`,
|
|
62
|
-
type: "text",
|
|
63
|
-
text: `;(globalThis as any).__lecodesSceneEdit = true\nexport {}\n`,
|
|
64
|
-
})
|
|
65
|
-
const editorImports = projectPaths
|
|
66
|
-
.filter((p) => p.endsWith(".editor.ts"))
|
|
67
|
-
.map((p) => `import "${p}"\n`)
|
|
68
|
-
.join("")
|
|
69
|
-
entries.push({
|
|
70
|
-
path: SCENE_ENTRY_PATH,
|
|
71
|
-
type: "text",
|
|
72
|
-
text: `import "${SCENE_HARNESS_DIR}/.edit-mode.ts"\n`
|
|
73
|
-
+ `import "${scenePath}"\n`
|
|
74
|
-
+ editorImports
|
|
75
|
-
+ `import "${SCENE_HARNESS_DIR}/main.ts"\n`,
|
|
76
|
-
})
|
|
77
|
-
return entries
|
|
78
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
// Helpers for serving the bundler's source map alongside the bundle. The map's generated positions
|
|
2
|
-
// are relative to the bundle code; when the deploy/preview prepends a comment header, the map must be
|
|
3
|
-
// shifted down by that many lines, then inlined into the bundle as a data URL (the preview executes
|
|
4
|
-
// the bundle via `new Function(string)`, which has no URL identity, so an external `.map` can't be
|
|
5
|
-
// fetched — it must be inline).
|
|
6
|
-
|
|
7
|
-
/** Shift every generated position in `mapJson` down by `lines` (each VLQ line is `;`-separated). */
|
|
8
|
-
export const offsetSourceMap = (mapJson: string, lines: number): string => {
|
|
9
|
-
if (lines <= 0) return mapJson
|
|
10
|
-
const map = JSON.parse(mapJson) as { mappings: string }
|
|
11
|
-
map.mappings = ";".repeat(lines) + map.mappings
|
|
12
|
-
return JSON.stringify(map)
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Inline the map into a bundle string under a **non-standard** directive (`lecodesMap`, not
|
|
17
|
-
* `sourceMappingURL`), so the browser does NOT auto-apply it. The bundle runs via `new Function`, and
|
|
18
|
-
* the browser maps the raw stack position *including* the `new Function` wrapper (~2-3 lines) which the
|
|
19
|
-
* map can't subtract — so its native source location would be off by those lines. Our preview mapper
|
|
20
|
-
* reads `lecodesMap` and subtracts the (calibrated) wrapper itself, so its locations are correct.
|
|
21
|
-
*/
|
|
22
|
-
export const inlineSourceMapComment = (mapJson: string): string => {
|
|
23
|
-
const b64 = Buffer.from(mapJson, "utf8").toString("base64")
|
|
24
|
-
return `//# lecodesMap=data:application/json;charset=utf-8;base64,${b64}`
|
|
25
|
-
}
|