lecodes-sdk 2.0.9 → 2.0.10

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/package.json CHANGED
@@ -45,7 +45,7 @@
45
45
  "marcidb-embedded": "^0.14.1",
46
46
  "sharp": "^0.35.2"
47
47
  },
48
- "version": "2.0.9",
48
+ "version": "2.0.10",
49
49
  "files": [
50
50
  "src",
51
51
  "dist",
package/prompts/core.md CHANGED
@@ -112,7 +112,7 @@ Everything in this prompt is a global — no imports needed. A project's only im
112
112
 
113
113
  Imports of the project's own files are also managed automatically: if your <edit> or <replace> makes code reference another file's export, the import is added for you — never fall back to a whole <file> rewrite just to change import lines. When writing a complete <file>, include imports normally.
114
114
 
115
- Reference a project asset (image, font, video, .svg, .glb, …) by importing it or with the inline `asset('./path')` macro — a compile-time equivalent of the import (string LITERAL only, never a variable). External `https://…` URLs are used directly as strings.
115
+ Reference a project asset (image, font, video, .svg, .glb, …) by its path as a string LITERAL — `UIImage("images/hero.png")`, `"./x.png"` from the file, `"/x.png"` from the root — or with the inline `asset('./path')` macro; both are a compile-time equivalent of the import (a literal, never a variable; a path that names no file fails the compile). A `.json` path stays a string. External `https://…` URLs are used directly as strings.
116
116
 
117
117
  ## Project context
118
118
 
package/prompts/ui.md CHANGED
@@ -161,7 +161,7 @@ UIImage(src) // src: string url | FetchResponse | File | SvgSource | Canvas (se
161
161
  // .setSourceRect(x, y, w, h) — crop to a sub-rect of the source (texture px); frame fills the box, objectFit
162
162
  // ignored. Chainable, safe to call before mount. For atlases/spritesheets — swap the rect to change frame.
163
163
  // Note: tintColor applies only to SVG sources — it replaces ALL fill and stroke colors (monochrome icons only)
164
- // Note: a bare relative path won't resolve — plain strings are https:// URLs; project files via import or asset('./photo.png')
164
+ // Note: a project file by its path as a string — UIImage("images/photo.png") — is the bundled asset (the compile resolves it; a path that names no file fails the compile); plain https:// strings are remote URLs
165
165
 
166
166
  // UIVideo — the on-screen surface only; playback lives entirely on the VideoPlayer (see AUDIO / VIDEO above)
167
167
  UIVideo(player)
@@ -466,6 +466,8 @@ UIImage(heart).style({ width: 24, height: 24, tintColor: "#666" }) // tintColor
466
466
  // ===== ICONS (assetIcon) =====
467
467
  // Use real icons instead of emoji. assetIcon("pack:name") resolves the icon at COMPILE time and
468
468
  // inlines it as an image source (same shape as SvgSource) — no imports, no project files, offline.
469
+ // It returns an SvgSource — the same type an imported .svg and SvgSource(xml) give — so a component
470
+ // that takes an icon is typed (icon: SvgSource), never (icon: string).
469
471
  UIImage(assetIcon("lucide:bell")).style({ width: 24, height: 24, tintColor: "#8a8f98" })
470
472
  // The id must be a string literal; an unknown id is a compile error (with name suggestions).
471
473
  // Recolor via the tintColor style or the { color } option (hex literal bakes in; expression tints).
@@ -520,12 +522,6 @@ UIRow(isGroup ? button : UIColumn()) // WRONG
520
522
  // ✅ null is skipped in children — no phantom element
521
523
  UIRow(isGroup ? button : null)
522
524
 
523
- // ❌ pointing UIImage / bgImage at a project file by bare path — it won't resolve to the bundled asset
524
- UIImage("./photo.png") // WRONG (a plain string works only for remote http(s) URLs)
525
- // ✅ import the asset, or wrap its path in asset()
526
- import photo from './photo.png'
527
- UIImage(photo) // or: UIImage(asset('./photo.png'))
528
-
529
525
  // ❌ callback-first edge callbacks on UIVirtualizedList
530
526
  list.onEndReached(loadNextPage) // WRONG — threshold is the first argument, and required
531
527
  // ✅ threshold (px from the edge) first
@@ -20,7 +20,7 @@ export const hasAssetMacro = (src: string): boolean => src.includes("asset(")
20
20
  * Not a full lexer: a regex LITERAL containing `asset(` would still be seen as code. That case
21
21
  * doesn't occur in practice and behaves exactly as it did before this masking existed.
22
22
  */
23
- const maskNonCode = (src: string): string => {
23
+ const maskNonCode = (src: string, onString?: (start: number, end: number) => void): string => {
24
24
  const out = src.split("")
25
25
  const blank = (from: number, to: number) => {
26
26
  for (let k = from; k < to && k < out.length; k++) if (out[k] !== "\n") out[k] = " "
@@ -57,12 +57,15 @@ const maskNonCode = (src: string): string => {
57
57
  if (c === "'" || c === "\"") {
58
58
  const start = i
59
59
  i++
60
+ let closed = false
60
61
  while (i < src.length) {
61
62
  const q = src[i]
62
63
  if (q === "\\") { i += 2; continue }
63
64
  i++
64
- if (q === c || q === "\n") break
65
+ if (q === c) { closed = true; break }
66
+ if (q === "\n") break
65
67
  }
68
+ if (closed && onString) onString(start, i)
66
69
  blank(start, i); continue
67
70
  }
68
71
  if (c === "`") { i++; scanTemplate(); continue }
@@ -77,8 +80,70 @@ const maskNonCode = (src: string): string => {
77
80
  return out.join("")
78
81
  }
79
82
 
80
- /** One `asset('…')` call site: the path as written, plus where it sits in the file. */
81
- export type AssetMacroRef = { path: string, index: number, length: number, line: number }
83
+ /** One `asset('…')` call site: the path as written, plus where it sits in the file. A bare path
84
+ * literal (`collectPathLiterals`) is the same shape: `path` is then the RESOLVED root path of the
85
+ * file it names, or, with `missing`, the literal as written — a path-looking string that names no
86
+ * file, reported by the bundler the way a missing `asset()` is. */
87
+ export type AssetMacroRef = { path: string, index: number, length: number, line: number, missing?: true }
88
+
89
+ /** The file kinds a bare path literal may stand for: what `asset()` returns a REFERENCE for (an
90
+ * image, a sound, a video, a model, a font, raw bytes) plus `.svg` (an SvgSource, what UIImage
91
+ * takes). Not `.json`, whose asset is its parsed data — a `.json` path in a string stays a string. */
92
+ 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
93
+ const CODE_EXT = /\.([tj]sx?|mjs|cjs|json)$/i
94
+ // What precedes an import specifier: the literal is a module path, never a resource reference.
95
+ const IMPORT_BEFORE = /(?:\bfrom|\bimport|\bimport\s*\(|\brequire\s*\()\s*$/
96
+
97
+ const normalizePath = (path: string): string => {
98
+ const out: string[] = []
99
+ for (const part of path.split("/")) {
100
+ if (part === "" || part === ".") continue
101
+ if (part === "..") { out.pop(); continue }
102
+ out.push(part)
103
+ }
104
+ return "/" + out.join("/")
105
+ }
106
+
107
+ /**
108
+ * Every string literal in `src` that is a PATH to a resource file of the project — `"images/hero.png"`,
109
+ * `"./hero.png"`, `"/sounds/tap.mp3"` — as the asset references they stand for: a bare path is the
110
+ * `asset('…')` call nobody had to write (2026-10-08: a model writes the path it put the file at and
111
+ * not the macro; the two now mean the same). Resolution: `./` and `../` from the file first, then
112
+ * from the project root (a model writes `./images/x.png` in `views/cart.ts` meaning the root);
113
+ * `/x` from the root; `a/b.png` from the root first, then the file. `exists(rootPath)` says whether
114
+ * the project has that file. A literal with a resource extension that starts with `./`, `../`, `/`
115
+ * or holds a `/` and names NO file comes back `missing` — the bundler reports it when the module
116
+ * is reached, the way it reports a missing `asset()`. A bare name (`"hero.png"`) that names no
117
+ * file is left as the string it is. Import specifiers, `asset()` arguments (`taken`: the ranges
118
+ * already claimed) and `.json` paths are never touched.
119
+ */
120
+ export const collectPathLiterals = (
121
+ src: string, filePath: string, exists: (rootPath: string) => boolean, taken: AssetMacroRef[] = [],
122
+ ): AssetMacroRef[] => {
123
+ const refs: AssetMacroRef[] = []
124
+ const dir = filePath.slice(0, filePath.lastIndexOf("/"))
125
+ maskNonCode(src, (start, end) => {
126
+ const value = src.slice(start + 1, end - 1)
127
+ if (value.length === 0 || value.length > 256) return
128
+ if (!BARE_PATH_EXT.test(value) || CODE_EXT.test(value)) return
129
+ if (/[\s:*?"'`<>|\\]/.test(value)) return // a url, an icon id, a sentence, a glob
130
+ if (taken.some((t) => start >= t.index && start < t.index + t.length)) return
131
+ if (IMPORT_BEFORE.test(src.slice(Math.max(0, start - 32), start))) return
132
+ const relative = value.startsWith("./") || value.startsWith("../")
133
+ const rooted = value.startsWith("/")
134
+ const candidates = rooted ? [ normalizePath(value) ]
135
+ : relative ? [ normalizePath(`${dir}/${value}`), normalizePath(value) ]
136
+ : [ normalizePath(value), normalizePath(`${dir}/${value}`) ]
137
+ const found = candidates.find(exists)
138
+ if (found === undefined && !relative && !rooted && !value.includes("/")) return
139
+ let line = 1
140
+ for (let k = 0; k < start; k++) if (src[k] === "\n") line++
141
+ refs.push(found !== undefined
142
+ ? { path: found, index: start, length: end - start, line }
143
+ : { path: value, index: start, length: end - start, line, missing: true })
144
+ })
145
+ return refs
146
+ }
82
147
 
83
148
  /**
84
149
  * Every `asset('…')` macro call in `src`, skipping the ones inside comments and string literals
@@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process"
3
3
  import { createRequire } from "node:module"
4
4
  import { dirname, join, relative } from "node:path"
5
5
  import { fileURLToPath } from "node:url"
6
- import { collectAssetMacros, desugarAssetMacros, hasAssetMacro, type AssetMacroRef } from "./assetMacro"
6
+ import { collectAssetMacros, collectPathLiterals, desugarAssetMacros, hasAssetMacro, type AssetMacroRef } from "./assetMacro"
7
7
  import { scanModulesFallback, type ScanModule } from "./detectEntry"
8
8
  import { hasAspectExtends, injectAspectNames } from "./aspectMacro"
9
9
  import { hasImportStatement, rewriteLibraryImports } from "./libraryImports"
@@ -178,6 +178,9 @@ const reachableFrom = (entry: string, sources: MemoryFiles): Set<string> => {
178
178
  const assertAssetsExist = (refs: { file: string, ref: AssetMacroRef }[], files: MemoryFiles) => {
179
179
  const missing: string[] = []
180
180
  for (const { file, ref } of refs) {
181
+ // A path literal that named no file of the project (collectPathLiterals): the string said
182
+ // where a file is, and it is not there.
183
+ if (ref.missing) { missing.push(`file not found: ${ref.path} (${file.replace(/^\//, "")}:${ref.line})`); continue }
181
184
  // Bare specifiers aren't paths at all — chisel reports those as bare imports.
182
185
  if (!ref.path.startsWith("./") && !ref.path.startsWith("../") && !ref.path.startsWith("/")) continue
183
186
  const dir = file.slice(0, file.lastIndexOf("/"))
@@ -216,8 +219,12 @@ const bundleProjectChisel = async (
216
219
  if (/\.[tj]sx?$/.test(path)) {
217
220
  if (hasImportStatement(src)) src = rewriteLibraryImports(src, slugs)
218
221
  scanSources[path] = src
219
- if (hasAssetMacro(src)) {
220
- const found = collectAssetMacros(src, path)
222
+ // `asset('…')` calls, then the bare path literals that name a resource of the project (the
223
+ // same reference without the macro); both desugar to the import the author could have written.
224
+ const macros = hasAssetMacro(src) ? collectAssetMacros(src, path) : []
225
+ const paths = collectPathLiterals(src, path, (rootPath) => files[rootPath] !== undefined, macros)
226
+ const found = macros.concat(paths).sort((a, b) => a.index - b.index)
227
+ if (found.length > 0) {
221
228
  for (const ref of found) assetRefs.push({ file: path, ref })
222
229
  src = desugarAssetMacros(src, path, found)
223
230
  }