@solidrt/cli 0.0.37 → 0.0.39
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 +9 -8
- package/scaffold/AGENTS.md +6 -0
- package/scaffold/icon.svg +11 -0
- package/scaffold/package.json +4 -4
- package/server/tsconfig.json +1 -0
- package/src/bundler.ts +5 -0
- package/src/commands/check.ts +72 -24
- package/src/commands/init.ts +4 -2
- package/src/commands/mcp.ts +6 -3
- package/src/commands/server.ts +15 -1
- package/src/dev-server.ts +3 -2
- package/src/pack-folder.ts +2 -1
- package/src/project.ts +45 -10
- package/src/untyped-deps.d.ts +18 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.39",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Antoine van Wel",
|
|
6
6
|
"type": "module",
|
|
@@ -22,23 +22,24 @@
|
|
|
22
22
|
"@jridgewell/remapping": "^2.3.0",
|
|
23
23
|
"@jridgewell/trace-mapping": "^0.3.25",
|
|
24
24
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
25
|
-
"babel-preset-solid": "2.0.0-beta.
|
|
25
|
+
"babel-preset-solid": "2.0.0-beta.26",
|
|
26
26
|
"bonjour-service": "^1.4.0",
|
|
27
27
|
"qrcode-generator": "^2.0.4",
|
|
28
28
|
"zod": "^4.4.3"
|
|
29
29
|
},
|
|
30
30
|
"optionalDependencies": {
|
|
31
|
-
"@solidrt/darwin-arm64": "0.0.
|
|
32
|
-
"@solidrt/linux-arm64-gnu": "0.0.
|
|
33
|
-
"@solidrt/linux-x64-gnu": "0.0.
|
|
34
|
-
"@solidrt/win32-x64-msvc": "0.0.
|
|
31
|
+
"@solidrt/darwin-arm64": "0.0.39",
|
|
32
|
+
"@solidrt/linux-arm64-gnu": "0.0.39",
|
|
33
|
+
"@solidrt/linux-x64-gnu": "0.0.39",
|
|
34
|
+
"@solidrt/win32-x64-msvc": "0.0.39"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@solidrt/core": "0.0.
|
|
37
|
+
"@solidrt/core": "0.0.39",
|
|
38
38
|
"typescript": "^7"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@solidrt/flux-types": "0.0.
|
|
41
|
+
"@solidrt/flux-types": "0.0.39",
|
|
42
|
+
"@types/babel__core": "^7.20.5",
|
|
42
43
|
"@types/bun": "latest"
|
|
43
44
|
}
|
|
44
45
|
}
|
package/scaffold/AGENTS.md
CHANGED
|
@@ -117,6 +117,12 @@ Authoritative references ship inside the installed packages - read them:
|
|
|
117
117
|
mounted. To inspect children (a typeof probe, counting), resolve them
|
|
118
118
|
first with the children() helper (re-exported from @solidrt/core) and
|
|
119
119
|
probe the resolved memo - never `typeof props.children` on the raw prop.
|
|
120
|
+
18. Cover/contain images: give `Image` a `fit` prop ("fill" | "cover" |
|
|
121
|
+
"contain" | "none" | "scale-down", CSS object-fit semantics, centered)
|
|
122
|
+
plus a box via `layout` in any form - numbers, pct(), flex. Without
|
|
123
|
+
`fit`, only NUMERIC layout sizes reach the image; `width: pct(100)`
|
|
124
|
+
alone draws at intrinsic size. `fit="cover"` is the answer for the
|
|
125
|
+
ported-web hero-image/thumbnail pattern.
|
|
120
126
|
|
|
121
127
|
## Performance model (JS is the slow lane)
|
|
122
128
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#5b7cfa"/>
|
|
5
|
+
<stop offset="1" stop-color="#8a5cf6"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect width="96" height="96" rx="22" fill="url(#bg)"/>
|
|
9
|
+
<circle cx="48" cy="48" r="21" fill="none" stroke="#ffffff" stroke-opacity="0.9" stroke-width="6"/>
|
|
10
|
+
<circle cx="48" cy="48" r="7" fill="#ffffff"/>
|
|
11
|
+
</svg>
|
package/scaffold/package.json
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
"android": "srt client --android"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@solidrt/core": "0.0.
|
|
13
|
-
"@solidrt/components": "0.0.
|
|
12
|
+
"@solidrt/core": "0.0.39",
|
|
13
|
+
"@solidrt/components": "0.0.39"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@solidrt/cli": "0.0.
|
|
17
|
-
"@solidrt/flux-types": "0.0.
|
|
16
|
+
"@solidrt/cli": "0.0.39",
|
|
17
|
+
"@solidrt/flux-types": "0.0.39",
|
|
18
18
|
"typescript": "^7"
|
|
19
19
|
}
|
|
20
20
|
}
|
package/server/tsconfig.json
CHANGED
package/src/bundler.ts
CHANGED
|
@@ -108,7 +108,12 @@ export type BundleResult = {
|
|
|
108
108
|
export async function bundleWith(opts: BundleOptions): Promise<BundleResult | null> {
|
|
109
109
|
// Define values are parsed as expressions, so string values need embedded
|
|
110
110
|
// quotes - a bare word substitutes as an identifier and crashes at runtime.
|
|
111
|
+
// import.meta.env.DEV is the solidrt build-mode constant (used by core's
|
|
112
|
+
// leak sentinel; typed in core's types.d.ts). NODE_ENV stays defined as
|
|
113
|
+
// ecosystem compat only: third-party libraries bundled into apps commonly
|
|
114
|
+
// read it, and an unresolved `process` crashes at import time.
|
|
111
115
|
let define: Record<string, string> = {
|
|
116
|
+
"import.meta.env.DEV": opts.dev ? "true" : "false",
|
|
112
117
|
"process.env.NODE_ENV": opts.dev ? '"development"' : '"production"',
|
|
113
118
|
}
|
|
114
119
|
if (opts.devBase) define.__SRT_DEV_BASE__ = opts.devBase
|
package/src/commands/check.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync } from "node:fs"
|
|
1
|
+
import { existsSync, mkdirSync, rmSync } from "node:fs"
|
|
2
2
|
import { dirname, join, resolve } from "node:path"
|
|
3
3
|
import { source } from "../args"
|
|
4
4
|
import { bundleWith } from "../bundler"
|
|
@@ -12,7 +12,7 @@ import { bundleWith } from "../bundler"
|
|
|
12
12
|
|
|
13
13
|
// Walk up from the entry to the enclosing project (tsconfig.json or, failing
|
|
14
14
|
// that, package.json).
|
|
15
|
-
function findProjectRoot(entry: string): string | null {
|
|
15
|
+
export function findProjectRoot(entry: string): string | null {
|
|
16
16
|
let dir = dirname(resolve(entry))
|
|
17
17
|
let byConfig: string | null = null
|
|
18
18
|
let byPackage: string | null = null
|
|
@@ -45,18 +45,77 @@ function parseDiagnostics(output: string): Diagnostic[] {
|
|
|
45
45
|
return diagnostics
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
// The project's tsc, found by walking up from the project root: an example
|
|
49
|
+
// app can carry a tsconfig without its own node_modules (monorepo case).
|
|
50
|
+
function findTsc(fromDir: string): string | null {
|
|
51
|
+
let dir = fromDir
|
|
52
|
+
while (true) {
|
|
53
|
+
let tsc = join(dir, "node_modules", ".bin", process.platform === "win32" ? "tsc.exe" : "tsc")
|
|
54
|
+
if (existsSync(tsc)) return tsc
|
|
55
|
+
let parent = dirname(dir)
|
|
56
|
+
if (parent === dir) return null
|
|
57
|
+
dir = parent
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Typecheck the entry's program, not the enclosing project: a transient
|
|
62
|
+
// config extends the project's tsconfig and roots the program at the entry
|
|
63
|
+
// alone, so tsc checks exactly the entry's import closure - unrelated files
|
|
64
|
+
// are excluded by construction. The config lives in the project-local
|
|
65
|
+
// .srt-data (the dev-artifact dir; absolute paths inside, so its location
|
|
66
|
+
// only matters for type-package resolution, which walks up to the project's
|
|
67
|
+
// node_modules from there).
|
|
68
|
+
export async function typecheck(root: string, entry: string): Promise<{ app: Diagnostic[]; hidden: number } | null> {
|
|
69
|
+
let tsconfig = join(root, "tsconfig.json")
|
|
70
|
+
if (!existsSync(tsconfig)) {
|
|
71
|
+
console.warn("Typecheck skipped: no tsconfig.json above the entry")
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
let tsc = findTsc(root)
|
|
75
|
+
if (!tsc) {
|
|
51
76
|
console.warn("Typecheck skipped: no tsc in the project (add the typescript devDependency)")
|
|
52
77
|
return null
|
|
53
78
|
}
|
|
54
|
-
let
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
79
|
+
let dataDir = join(root, ".srt-data")
|
|
80
|
+
mkdirSync(dataDir, { recursive: true })
|
|
81
|
+
let config = join(dataDir, `typecheck-${process.pid}.tsconfig.json`)
|
|
82
|
+
// include: [] overrides any include inherited from the extended config -
|
|
83
|
+
// files and include are unioned, so without this a base config's include
|
|
84
|
+
// would drag the whole project back into the program.
|
|
85
|
+
await Bun.write(config, JSON.stringify({ extends: tsconfig, include: [], files: [resolve(entry)] }))
|
|
86
|
+
try {
|
|
87
|
+
let proc = Bun.spawn([tsc, "-p", config, "--noEmit", "--pretty", "false"], {
|
|
88
|
+
cwd: root,
|
|
89
|
+
stdout: "pipe",
|
|
90
|
+
stderr: "pipe",
|
|
91
|
+
})
|
|
92
|
+
let [out, err] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()])
|
|
93
|
+
await proc.exited
|
|
94
|
+
let diagnostics = parseDiagnostics(out + err)
|
|
95
|
+
let app = diagnostics.filter((d) => !d.inDependencies)
|
|
96
|
+
return { app, hidden: diagnostics.length - app.length }
|
|
97
|
+
} finally {
|
|
98
|
+
rmSync(config, { force: true })
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Print a typecheck result (diagnostics, then the one-line verdict) and return
|
|
103
|
+
// whether app-code errors were found. Callers pass repl-aware printers when
|
|
104
|
+
// the output lands over a live prompt (the dev-server startup check).
|
|
105
|
+
export function reportTypes(
|
|
106
|
+
types: { app: Diagnostic[]; hidden: number },
|
|
107
|
+
log: (...args: any[]) => void = console.log,
|
|
108
|
+
error: (...args: any[]) => void = console.error,
|
|
109
|
+
): boolean {
|
|
110
|
+
for (let d of types.app) error(d.lines.join("\n"))
|
|
111
|
+
if (types.app.length > 0) {
|
|
112
|
+
let hidden = types.hidden > 0 ? ` (${types.hidden} in dependencies hidden)` : ""
|
|
113
|
+
error(`${types.app.length} type error${types.app.length === 1 ? "" : "s"} in app code${hidden}`)
|
|
114
|
+
return true
|
|
115
|
+
}
|
|
116
|
+
if (types.hidden > 0) log(`Types OK (${types.hidden} dependency-internal errors hidden)`)
|
|
117
|
+
else log("Types OK")
|
|
118
|
+
return false
|
|
60
119
|
}
|
|
61
120
|
|
|
62
121
|
export async function runCheckCommand() {
|
|
@@ -73,19 +132,8 @@ export async function runCheckCommand() {
|
|
|
73
132
|
if (!root) {
|
|
74
133
|
console.warn("Typecheck skipped: no tsconfig.json or package.json above the entry")
|
|
75
134
|
} else {
|
|
76
|
-
let types = await typecheck(root)
|
|
77
|
-
if (types)
|
|
78
|
-
for (let d of types.app) console.error(d.lines.join("\n"))
|
|
79
|
-
if (types.app.length > 0) {
|
|
80
|
-
failed = true
|
|
81
|
-
let hidden = types.hidden > 0 ? ` (${types.hidden} in dependencies hidden)` : ""
|
|
82
|
-
console.error(`${types.app.length} type error${types.app.length === 1 ? "" : "s"} in app code${hidden}`)
|
|
83
|
-
} else if (types.hidden > 0) {
|
|
84
|
-
console.log(`Types OK (${types.hidden} dependency-internal errors hidden)`)
|
|
85
|
-
} else {
|
|
86
|
-
console.log("Types OK")
|
|
87
|
-
}
|
|
88
|
-
}
|
|
135
|
+
let types = await typecheck(root, entry)
|
|
136
|
+
if (types && reportTypes(types)) failed = true
|
|
89
137
|
}
|
|
90
138
|
|
|
91
139
|
if (failed) process.exit(1)
|
package/src/commands/init.ts
CHANGED
|
@@ -141,9 +141,11 @@ export async function runInitCommand() {
|
|
|
141
141
|
|
|
142
142
|
// The assets/ convention folder, created up front: everything in it ships
|
|
143
143
|
// with the app, and the dev watcher only picks up an assets/ folder that
|
|
144
|
-
// exists when it starts.
|
|
144
|
+
// exists when it starts. It starts with a placeholder app icon (picked up
|
|
145
|
+
// through the assets/icon.svg convention) for the author to replace.
|
|
145
146
|
await mkdir(join(dir, "assets"), { recursive: true })
|
|
146
|
-
|
|
147
|
+
await writeFile(join(dir, "assets", "icon.svg"), await readFile(join(SCAFFOLD_DIR, "icon.svg")))
|
|
148
|
+
console.log(" Write assets/icon.svg")
|
|
147
149
|
|
|
148
150
|
// The scaffold package.json carries a placeholder name; set it from the
|
|
149
151
|
// target folder. A core-level app gets no component framework dependency.
|
package/src/commands/mcp.ts
CHANGED
|
@@ -103,7 +103,7 @@ let TOOLS: {
|
|
|
103
103
|
name: "get_stats",
|
|
104
104
|
readOnly: true,
|
|
105
105
|
description:
|
|
106
|
-
"Performance statistics from a running app client: fps, CPU%, memory, smoothed JS/layout/paint/hover frame times (ms), setProperty writes per frame, demand-gate reuse/skip counts per second, and live texture count. Layout-activity counters cover the last full rebuild, raw: nodes (live node count, mounted AND detached), mountedNodes/orphanNodes (live at query time: nodes reachable from the root vs not - orphans growing at a stable tree shape mean an unmount leak; absent when no engine is running), measureCalls (text measures; mostly cache hits, cheap), paraShapes (paragraphs actually shaped; the expensive signal - high layoutMs with near-zero paraShapes means the cost is not text shaping), dirtiedNodes (layout caches cleared by property writes since the previous rebuild; how much of the tree a write burst invalidated), cacheGets/cacheHits (layout-cache lookups during the rebuild; a hit on a container skips its whole subtree, so a healthy incremental rebuild shows a near-100% hit rate - a low rate at scale means the layout cache is being defeated).",
|
|
106
|
+
"Performance statistics from a running app client: fps, CPU%, memory, smoothed JS/layout/paint/hover frame times (ms), setProperty writes per frame, demand-gate reuse/skip counts per second, and live texture count. Layout-activity counters cover the last full rebuild, raw: nodes (live node count, mounted AND detached), mountedNodes/orphanNodes (live at query time: nodes reachable from the root vs not - orphans growing at a stable tree shape mean an unmount leak; absent when no engine is running), measureCalls (text measures; mostly cache hits, cheap), paraShapes (paragraphs actually shaped; the expensive signal - high layoutMs with near-zero paraShapes means the cost is not text shaping), dirtiedNodes (layout caches cleared by property writes since the previous rebuild; how much of the tree a write burst invalidated), cacheGets/cacheHits (layout-cache lookups during the rebuild; a hit on a container skips its whole subtree, so a healthy incremental rebuild shows a near-100% hit rate - a low rate at scale means the layout cache is being defeated). GPU-side health, read live at query time (absent when no engine is running): rasterQueue (raster commands sent but not yet executed; stuck nonzero means the raster thread is backlogged - the state where fps and frameMs go blind because no frames complete), idleTicks (cumulative idle frame signals emitted while the GPU had nothing queued; idleTicks racing while rasterQueue sits nonzero would mean the idle-tick gate is broken), fenceTimeouts (cumulative present-fence waits that expired instead of signaling - each one is a frame where the GPU was over budget for 100ms+ and one-frame-in-flight pacing was lost; zero on a healthy machine, climbing means the GPU is the bottleneck right now).",
|
|
107
107
|
inputSchema: { client: CLIENT_ARG },
|
|
108
108
|
},
|
|
109
109
|
{
|
|
@@ -137,9 +137,12 @@ let TOOLS: {
|
|
|
137
137
|
name: "get_snapshot",
|
|
138
138
|
readOnly: true,
|
|
139
139
|
description:
|
|
140
|
-
"Capture a PNG image of any node in a running app client's render tree, by node id (get ids from get_render_tree). Returns the rendered pixels of that node's subtree, so you can see what the app actually drew. The node must be currently mounted and have a non-zero layout box. Works on an idle client (the capture requests its own frame); a timeout means the client's JS thread is busy or wedged, not that the app is idle.",
|
|
140
|
+
"Capture a PNG image of any node in a running app client's render tree, by node id (get ids from get_render_tree). Returns the rendered pixels of that node's subtree, so you can see what the app actually drew. Capture the smallest node that contains what you are checking (e.g. the <texture> leaf itself) - that is exactly the content at its own pixel size; the window root is mostly empty layout around it and orders of magnitude more pixels. Reserve root captures for when layout/positioning itself is the question. The node must be currently mounted and have a non-zero layout box. Works on an idle client (the capture requests its own frame); a timeout means the client's JS thread is busy or wedged, not that the app is idle.",
|
|
141
141
|
inputSchema: {
|
|
142
|
-
nodeId: z
|
|
142
|
+
nodeId: z
|
|
143
|
+
.number()
|
|
144
|
+
.int()
|
|
145
|
+
.describe("Id of the node to capture, from get_render_tree; prefer the smallest relevant node over the root"),
|
|
143
146
|
save_to: SAVE_TO_ARG,
|
|
144
147
|
client: CLIENT_ARG,
|
|
145
148
|
},
|
package/src/commands/server.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import pkg from "../../package.json"
|
|
2
2
|
import { source, isSource, isPrebuilt, values } from "../args"
|
|
3
|
-
import { state, shutdown } from "../util"
|
|
3
|
+
import { state, shutdown, print, printErr } from "../util"
|
|
4
|
+
import { findProjectRoot, typecheck, reportTypes } from "./check"
|
|
4
5
|
import { bundle } from "../bundler"
|
|
5
6
|
import { buildManifest, projectDirFor } from "../project"
|
|
6
7
|
import { startServer, buildReload, sendReload, showBuildFailure } from "../dev-server"
|
|
@@ -53,4 +54,17 @@ export async function runServerCommand() {
|
|
|
53
54
|
console.log(`[cli] Welcome to SolidRT${version}!`)
|
|
54
55
|
startRepl()
|
|
55
56
|
startWatcher()
|
|
57
|
+
|
|
58
|
+
// Startup typecheck, deliberately not awaited: diagnostics print over the
|
|
59
|
+
// repl when tsc finishes, and a type error never gates the boot (srt check
|
|
60
|
+
// is the hard gate). Once per server lifetime; hot reloads never typecheck.
|
|
61
|
+
// Source builds only: a prebuilt .srt.js has no checkable project here.
|
|
62
|
+
if (source && isSource) startupTypecheck(source)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function startupTypecheck(entry: string) {
|
|
66
|
+
let root = findProjectRoot(entry)
|
|
67
|
+
if (!root) return
|
|
68
|
+
let types = await typecheck(root, entry)
|
|
69
|
+
if (types) reportTypes(types, print, printErr)
|
|
56
70
|
}
|
package/src/dev-server.ts
CHANGED
|
@@ -82,7 +82,8 @@ export async function watchAllowed(): Promise<boolean> {
|
|
|
82
82
|
try {
|
|
83
83
|
let resp = await fetch(`${INTERNAL_BASE}/watch`)
|
|
84
84
|
if (!resp.ok) return true
|
|
85
|
-
|
|
85
|
+
let data = (await resp.json()) as { enabled?: boolean }
|
|
86
|
+
return data.enabled !== false
|
|
86
87
|
} catch {
|
|
87
88
|
return true
|
|
88
89
|
}
|
|
@@ -100,7 +101,7 @@ export type ClientEntry = {
|
|
|
100
101
|
export async function getClients(): Promise<ClientEntry[]> {
|
|
101
102
|
let resp = await fetch(`${INTERNAL_BASE}/clients`)
|
|
102
103
|
if (!resp.ok) throw new Error(`Dev server /clients failed: ${resp.status}`)
|
|
103
|
-
return resp.json()
|
|
104
|
+
return resp.json() as Promise<ClientEntry[]>
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
// Reload code that fails to start the engine on purpose. The runtime treats a
|
package/src/pack-folder.ts
CHANGED
|
@@ -37,7 +37,7 @@ export type PackFolder = {
|
|
|
37
37
|
export function buildPackFolder(entry: string, bytecode: Buffer): PackFolder {
|
|
38
38
|
let identity = loadAppIdentity(entry)
|
|
39
39
|
let projectDir = projectDirFor(resolve(entry))
|
|
40
|
-
let { assets } = collectAssets(entry)
|
|
40
|
+
let { assets, icon } = collectAssets(entry)
|
|
41
41
|
let copies = assets.map((a) => ({ from: join(projectDir, a.path), to: a.path }))
|
|
42
42
|
|
|
43
43
|
// The full resolved font set: custom fonts are already collected assets;
|
|
@@ -70,6 +70,7 @@ export function buildPackFolder(entry: string, bytecode: Buffer): PackFolder {
|
|
|
70
70
|
appId: identity.appId,
|
|
71
71
|
org: identity.org,
|
|
72
72
|
displayName: identity.displayName,
|
|
73
|
+
...(icon ? { icon } : {}),
|
|
73
74
|
runtimeVersion: RUNTIME_VERSION,
|
|
74
75
|
solidrtVersion: SOLIDRT_VERSION,
|
|
75
76
|
bundle: { path: "bundle.bin", sha256: hashHex(bytecode), size: bytecode.length },
|
package/src/project.ts
CHANGED
|
@@ -8,7 +8,10 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "nod
|
|
|
8
8
|
// "appId": "com.example.app", // stable identity: storage dir, Android package id
|
|
9
9
|
// "org": "Example", // optional display metadata (publisher)
|
|
10
10
|
// "displayName": "Example App", // optional display metadata (launcher/window)
|
|
11
|
-
// "fonts": { ... }
|
|
11
|
+
// "fonts": { ... }, // see fonts.ts
|
|
12
|
+
// "icon": "./assets/icon.svg" // optional app icon (SVG, under assets/);
|
|
13
|
+
// // an undeclared assets/icon.svg is picked
|
|
14
|
+
// // up by convention
|
|
12
15
|
// }
|
|
13
16
|
//
|
|
14
17
|
// Everything defaults from the package name (or the entry filename when there
|
|
@@ -71,11 +74,13 @@ export const SOLIDRT_VERSION: string = pkgVersion === "0.0.0" ? "unknown" : pkgV
|
|
|
71
74
|
export function buildManifest(code: string, entry: string): string {
|
|
72
75
|
let identity = loadAppIdentity(entry)
|
|
73
76
|
let sha256 = new Bun.CryptoHasher("sha256").update(code).digest("hex")
|
|
74
|
-
let { assets, fonts } = collectAssets(entry)
|
|
77
|
+
let { assets, fonts, icon } = collectAssets(entry)
|
|
75
78
|
return JSON.stringify({
|
|
76
79
|
appId: identity.appId,
|
|
80
|
+
displayName: identity.displayName,
|
|
77
81
|
runtimeVersion: RUNTIME_VERSION,
|
|
78
82
|
solidrtVersion: SOLIDRT_VERSION,
|
|
83
|
+
...(icon ? { icon } : {}),
|
|
79
84
|
bundle: { path: "bundle.js", sha256, size: Buffer.byteLength(code, "utf8") },
|
|
80
85
|
...(assets.length ? { assets } : {}),
|
|
81
86
|
...(fonts.length ? { fonts } : {}),
|
|
@@ -116,11 +121,19 @@ function walkAssets(assetsDir: string, dir: string, out: ManifestAsset[]) {
|
|
|
116
121
|
|
|
117
122
|
// The convention-first asset set: everything under the project's assets/
|
|
118
123
|
// folder (next to package.json), collected wholesale in sorted order so the
|
|
119
|
-
// manifest bytes are deterministic. Fonts
|
|
120
|
-
// set: `solidrt.fonts` path entries
|
|
121
|
-
//
|
|
122
|
-
// have no manifest
|
|
123
|
-
|
|
124
|
+
// manifest bytes are deterministic. Fonts and the icon are annotations
|
|
125
|
+
// pointing into that set: `solidrt.fonts` path entries and `solidrt.icon`
|
|
126
|
+
// must live under assets/ so they reach dev clients and the version store
|
|
127
|
+
// (`false` font entries only drop pack defaults and have no manifest
|
|
128
|
+
// presence). The icon is SVG-only for now: the launcher renders SVG natively,
|
|
129
|
+
// and the raster surfaces (window icon, OS embedding) come with later stages
|
|
130
|
+
// (okf/backlog/app-icons.md). An undeclared assets/icon.svg is picked up by
|
|
131
|
+
// convention.
|
|
132
|
+
export function collectAssets(entry: string): {
|
|
133
|
+
assets: ManifestAsset[]
|
|
134
|
+
fonts: ManifestFont[]
|
|
135
|
+
icon: string | null
|
|
136
|
+
} {
|
|
124
137
|
let project = findProjectPackage(entry)
|
|
125
138
|
let projectDir = projectDirFor(entry)
|
|
126
139
|
let assetsDir = resolve(projectDir, "assets")
|
|
@@ -146,7 +159,27 @@ export function collectAssets(entry: string): { assets: ManifestAsset[]; fonts:
|
|
|
146
159
|
fonts.push({ path, alias })
|
|
147
160
|
}
|
|
148
161
|
}
|
|
149
|
-
|
|
162
|
+
|
|
163
|
+
let icon: string | null = null
|
|
164
|
+
let declared = project?.pkg.solidrt?.icon
|
|
165
|
+
if (declared !== undefined) {
|
|
166
|
+
if (typeof declared !== "string") fail('"solidrt": "icon" must be a string path')
|
|
167
|
+
let path = assetPathFor(projectDir, resolve(projectDir, declared))
|
|
168
|
+
if (!path) {
|
|
169
|
+
fail(`"solidrt.icon": ${declared} must live under assets/ (the icon ships as a version asset)`)
|
|
170
|
+
}
|
|
171
|
+
if (!path.toLowerCase().endsWith(".svg")) {
|
|
172
|
+
fail(`"solidrt.icon": ${declared} must be an .svg file`)
|
|
173
|
+
}
|
|
174
|
+
if (!assets.some((a) => a.path === path)) {
|
|
175
|
+
fail(`"solidrt.icon": no such file: ${resolve(projectDir, declared)}`)
|
|
176
|
+
}
|
|
177
|
+
icon = path
|
|
178
|
+
} else if (assets.some((a) => a.path === "assets/icon.svg")) {
|
|
179
|
+
icon = "assets/icon.svg"
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { assets, fonts, icon }
|
|
150
183
|
}
|
|
151
184
|
|
|
152
185
|
// Resolve the app identity for a pack. All three fields are guaranteed
|
|
@@ -154,7 +187,9 @@ export function collectAssets(entry: string): { assets: ManifestAsset[]; fonts:
|
|
|
154
187
|
export function loadAppIdentity(sourcePath: string): AppIdentity {
|
|
155
188
|
let project = findProjectPackage(sourcePath)
|
|
156
189
|
let config = project?.pkg.solidrt ?? {}
|
|
157
|
-
|
|
190
|
+
// A scoped package name (@org/name) defaults to its last segment: identity
|
|
191
|
+
// fields reject path separators, and derived defaults must never fail that.
|
|
192
|
+
let fallbackName = (project?.pkg.name ?? basename(sourcePath).replace(/\.[jt]sx?$/, "")).split("/").pop()!
|
|
158
193
|
|
|
159
194
|
for (let key of ["appId", "org", "displayName"]) {
|
|
160
195
|
if (key in config && typeof config[key] !== "string") fail(`"solidrt": "${key}" must be a string`)
|
|
@@ -170,7 +205,7 @@ export function loadAppIdentity(sourcePath: string): AppIdentity {
|
|
|
170
205
|
fail(`"solidrt": "appId" must match ${APP_ID_PATTERN} (reverse-DNS recommended, e.g. "com.example.app")`)
|
|
171
206
|
}
|
|
172
207
|
}
|
|
173
|
-
let displayName = config.displayName ??
|
|
208
|
+
let displayName = config.displayName ?? fallbackName
|
|
174
209
|
let org = config.org ?? displayName
|
|
175
210
|
checkField(appId, '"appId"')
|
|
176
211
|
checkField(displayName, '"displayName"')
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Packages with no bundled or DefinitelyTyped declarations. @babel/core is
|
|
2
|
+
// covered by @types/babel__core; plugins and presets have no dedicated types
|
|
3
|
+
// by convention - they are consumed as opaque PluginItem values.
|
|
4
|
+
declare module "@babel/plugin-syntax-jsx" {
|
|
5
|
+
import { type PluginItem } from "@babel/core"
|
|
6
|
+
let plugin: PluginItem
|
|
7
|
+
export default plugin
|
|
8
|
+
}
|
|
9
|
+
declare module "@babel/preset-typescript" {
|
|
10
|
+
import { type PluginItem } from "@babel/core"
|
|
11
|
+
let preset: PluginItem
|
|
12
|
+
export default preset
|
|
13
|
+
}
|
|
14
|
+
declare module "babel-preset-solid" {
|
|
15
|
+
import { type PluginItem } from "@babel/core"
|
|
16
|
+
let preset: PluginItem
|
|
17
|
+
export default preset
|
|
18
|
+
}
|