lecodes-cli 0.10.1 → 0.10.3

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.
@@ -1,52 +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
- // Legacy worker-module imports (`import { UIScreen } from "ui"`) predate the globals-only SDK —
40
- // chisel would reject them anyway; say so here instead of "Unknown library".
41
- if (slug === "ui" || slug === "creator" || slug === "utils") {
42
- throw new Error(`import from "${slug}": the SDK needs no imports — UIScreen, Scene, etc. are globals; remove this import line.`)
43
- }
44
- throw new Error(`Unknown library "${slug}" — add it in Settings → Libraries, or use a relative path like "./${slug}".`)
45
- }
46
-
47
- let out = src.replace(IMPORT_FROM_RE, (_m, boundary, head, quote, spec) =>
48
- `${boundary}${head}${quote}${rewriteSpec(spec)}${quote}`)
49
- out = out.replace(SIDE_EFFECT_RE, (m, boundary, head, quote, spec) =>
50
- isRelativeOrAbsolute(spec) ? m : `${boundary}${head}${quote}${rewriteSpec(spec)}${quote}`)
51
- return out
52
- }
@@ -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,98 +0,0 @@
1
- // Minimal sfnt (TTF/OTF) reader for the `font()` compile macro: family name from the `name` table,
2
- // weight/italic from `OS/2` (with a `head.macStyle` fallback). Reads only what the macro needs —
3
- // this is not a font sanitizer or validator.
4
-
5
- export type SfntInfo = { family: string, weight: number, italic: boolean }
6
-
7
- const TAG_TTF = 0x00010000
8
- const TAG_OTTO = 0x4f54544f // 'OTTO' (CFF outlines)
9
- const TAG_TTCF = 0x74746366 // 'ttcf' (collection)
10
- const TAG_WOFF = 0x774f4646 // 'wOFF'
11
- const TAG_WOF2 = 0x774f4632 // 'wOF2'
12
-
13
- /** Parse a .ttf/.otf. Throws a message meant to surface verbatim in a compile error. */
14
- export const parseSfnt = (bytes: Uint8Array): SfntInfo => {
15
- const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
16
- if (bytes.byteLength < 12) throw new Error("not a font file (truncated)")
17
- const version = dv.getUint32(0)
18
- if (version === TAG_TTCF) throw new Error("font collections (.ttc) aren't supported — export a single face")
19
- if (version === TAG_WOFF || version === TAG_WOF2) throw new Error("WOFF/WOFF2 project fonts aren't supported — use the .ttf/.otf")
20
- if (version !== TAG_TTF && version !== TAG_OTTO) throw new Error("not a TTF/OTF font file")
21
-
22
- const numTables = dv.getUint16(4)
23
- const tables = new Map<string, { offset: number, length: number }>()
24
- for (let i = 0; i < numTables; i++) {
25
- const rec = 12 + i * 16
26
- if (rec + 16 > bytes.byteLength) throw new Error("not a font file (truncated table directory)")
27
- const tag = String.fromCharCode(bytes[rec], bytes[rec + 1], bytes[rec + 2], bytes[rec + 3])
28
- tables.set(tag, { offset: dv.getUint32(rec + 8), length: dv.getUint32(rec + 12) })
29
- }
30
-
31
- const family = readFamily(dv, bytes, tables.get("name"))
32
- if (!family) throw new Error("no family name in the font's `name` table")
33
-
34
- let weight = 400
35
- let italic = false
36
- const os2 = tables.get("OS/2")
37
- if (os2 && os2.offset + 64 <= bytes.byteLength) {
38
- weight = dv.getUint16(os2.offset + 4) // usWeightClass
39
- italic = (dv.getUint16(os2.offset + 62) & 1) === 1 // fsSelection ITALIC
40
- } else {
41
- const head = tables.get("head")
42
- if (head && head.offset + 46 <= bytes.byteLength) {
43
- const macStyle = dv.getUint16(head.offset + 44)
44
- if (macStyle & 1) weight = 700
45
- italic = (macStyle & 2) === 2
46
- }
47
- }
48
- if (weight < 1 || weight > 1000) weight = 400
49
-
50
- return { family, weight, italic }
51
- }
52
-
53
- /** Best family name: typographic family (nameID 16) over legacy family (1); Windows-English
54
- * records over the rest. Returns "" when the table yields nothing decodable. */
55
- const readFamily = (dv: DataView, bytes: Uint8Array, name?: { offset: number, length: number }): string => {
56
- if (!name || name.offset + 6 > bytes.byteLength) return ""
57
- const base = name.offset
58
- const count = dv.getUint16(base + 2)
59
- const stringOffset = base + dv.getUint16(base + 4)
60
-
61
- let best = ""
62
- let bestScore = -1
63
- for (let i = 0; i < count; i++) {
64
- const rec = base + 6 + i * 12
65
- if (rec + 12 > bytes.byteLength) break
66
- const platformID = dv.getUint16(rec)
67
- const languageID = dv.getUint16(rec + 4)
68
- const nameID = dv.getUint16(rec + 6)
69
- const length = dv.getUint16(rec + 8)
70
- const offset = stringOffset + dv.getUint16(rec + 10)
71
- if (nameID !== 16 && nameID !== 1) continue
72
- if (offset + length > bytes.byteLength) continue
73
-
74
- // nameID 16 beats 1; within an id, Windows-English > Windows > Unicode > Mac.
75
- let score = nameID === 16 ? 100 : 0
76
- if (platformID === 3) score += languageID === 0x409 ? 30 : 20
77
- else if (platformID === 0) score += 10
78
- if (score <= bestScore) continue
79
-
80
- const text = platformID === 1 ? decodeLatin1(bytes, offset, length) : decodeUtf16be(bytes, offset, length)
81
- if (!text.trim()) continue
82
- best = text.trim()
83
- bestScore = score
84
- }
85
- return best
86
- }
87
-
88
- const decodeUtf16be = (bytes: Uint8Array, offset: number, length: number): string => {
89
- let out = ""
90
- for (let i = 0; i + 1 < length; i += 2) out += String.fromCharCode((bytes[offset + i] << 8) | bytes[offset + i + 1])
91
- return out
92
- }
93
-
94
- const decodeLatin1 = (bytes: Uint8Array, offset: number, length: number): string => {
95
- let out = ""
96
- for (let i = 0; i < length; i++) out += String.fromCharCode(bytes[offset + i])
97
- return out
98
- }
@@ -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
- }