@solidrt/cli 0.0.26 → 0.0.28

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.
@@ -0,0 +1,54 @@
1
+ import { command } from "flux:subprocess"
2
+ import { state } from "./state"
3
+
4
+ // Server-owned "rebuild and push": the single place the running app is rebuilt
5
+ // from source on demand (an MCP reload). The srt repl still bundles in-process
6
+ // for its own keystroke reloads, but both routes call the same bundle-cli, so
7
+ // the bundling logic cannot drift. Making the server the rebuild authority is
8
+ // the interim step toward folding the whole CLI into flux (see
9
+ // okf/backlog/cli-flux-migration.md).
10
+
11
+ // Build the reload message the same way srt's buildReload does, so a
12
+ // server-triggered reload is indistinguishable from a repl-triggered one to
13
+ // clients. proxyFiles/proxyHttp are message flags, not build inputs.
14
+ function buildReload(code: string) {
15
+ let config = state.config
16
+ return { type: "reload", proxyFiles: config.proxyFiles, proxyHttp: config.proxyHttp, code }
17
+ }
18
+
19
+ // Rebuild from state.config.entry via the external bundle-cli subprocess, then
20
+ // latch (for late-joining clients) and broadcast the reload to every connected
21
+ // client. Resolves with an error message on failure (no entry configured, or a
22
+ // build error), or null on success.
23
+ export async function rebuildAndBroadcast(): Promise<string | null> {
24
+ let config = state.config
25
+ if (!config.entry) {
26
+ return "No app entry to rebuild. Start srt with a source file (srt run src/index.tsx)."
27
+ }
28
+
29
+ let params = JSON.stringify({
30
+ entry: config.entry,
31
+ devBase: state.serverUrl,
32
+ dev: true,
33
+ minify: config.minify,
34
+ })
35
+
36
+ let result = await command(config.bundlerCmd[0]!, [...config.bundlerCmd.slice(1), params]).output()
37
+ if (!result.success) {
38
+ let stderr = typeof result.stderr === "string" ? result.stderr : ""
39
+ return `Rebuild failed:\n${stderr.trim()}`
40
+ }
41
+
42
+ // bundle-cli writes one JSON object { code, map } to stdout.
43
+ let bundle: { code?: string; map?: string | null }
44
+ try {
45
+ bundle = JSON.parse(typeof result.stdout === "string" ? result.stdout : "")
46
+ } catch {
47
+ return "Rebuild failed: unreadable bundler output"
48
+ }
49
+ state.currentMap = bundle.map ?? null
50
+ let text = JSON.stringify(buildReload(bundle.code ?? ""))
51
+ state.currentReload = text
52
+ for (let ws of state.clients.keys()) ws.send(text)
53
+ return null
54
+ }
@@ -0,0 +1,47 @@
1
+ import { TraceMap, originalPositionFor } from "@jridgewell/trace-mapping"
2
+
3
+ // Stack-trace remapping for forwarded client logs. The runtime evaluates the
4
+ // bundle as module "main", so QuickJS frames cite bundle positions like
5
+ // "at boom (main:212:9)". With the current reload's sourcemap latched on the
6
+ // server (state.currentMap), those positions are rewritten to the original
7
+ // .tsx sources before a log entry is buffered.
8
+
9
+ // The parsed map is cached per map text; a reload swaps the text and the next
10
+ // lookup rebuilds the tracer.
11
+ let cachedText: string | null = null
12
+ let tracer: TraceMap | null = null
13
+
14
+ function tracerFor(map: string | null): TraceMap | null {
15
+ if (map !== cachedText) {
16
+ cachedText = map
17
+ tracer = null
18
+ if (map) {
19
+ try {
20
+ tracer = new TraceMap(JSON.parse(map))
21
+ } catch {
22
+ // A malformed map disables remapping until the next reload.
23
+ }
24
+ }
25
+ }
26
+ return tracer
27
+ }
28
+
29
+ /**
30
+ * Rewrite every "main:LINE:COL" (or "main:LINE") position in `text` to its
31
+ * original source position, e.g. "src/app.tsx:42:7". Positions the map has no
32
+ * entry for, and all text when `map` is null, pass through unchanged.
33
+ * QuickJS lines and columns are 1-based; sourcemap columns are 0-based.
34
+ */
35
+ export function remapPositions(text: string, map: string | null): string {
36
+ if (!map || !text.includes("main:")) return text
37
+ let t = tracerFor(map)
38
+ if (!t) return text
39
+ return text.replace(/\bmain:(\d+)(?::(\d+))?\b/g, (frame, line, column) => {
40
+ let pos = originalPositionFor(t, {
41
+ line: parseInt(line, 10),
42
+ column: column ? Math.max(parseInt(column, 10) - 1, 0) : 0,
43
+ })
44
+ if (pos.source == null || pos.line == null) return frame
45
+ return `${pos.source}:${pos.line}:${(pos.column ?? 0) + 1}`
46
+ })
47
+ }
package/server/state.ts CHANGED
@@ -11,6 +11,15 @@ export type Config = {
11
11
  address: string
12
12
  proxyFiles: boolean
13
13
  proxyHttp: boolean
14
+ /** The app entry (absolute .tsx/.jsx path) the server rebuilds on an
15
+ * MCP-triggered reload, or undefined when srt was started without a source.
16
+ * Moved by the repl `load` command via /__internal__/reload. */
17
+ entry?: string
18
+ /** Minify the rebuild output, mirroring the srt --minify flag. */
19
+ minify: boolean
20
+ /** How the server invokes the external bundler: [bunPath, bundleCliPath],
21
+ * spawned with a JSON params argument appended (see rebuild.ts). */
22
+ bundlerCmd: string[]
14
23
  /** Enable the sqlite-backed proxy cache. */
15
24
  cache: boolean
16
25
  /** Directory holding .srt-cache.db. */
@@ -22,17 +31,31 @@ export type Config = {
22
31
  tunnel: boolean
23
32
  }
24
33
 
25
- export type ClientInfo = { platform: string; version: string; id: number; capabilities: string[] }
34
+ export type ClientInfo = { platform: string; version: string; profile: string; id: number; capabilities: string[] }
26
35
 
27
36
  export let state = {
28
37
  config: undefined as unknown as Config,
29
38
  clients: new Map<ServerWebSocket, ClientInfo>(),
30
39
  nextClientId: 0,
40
+ /**
41
+ * Identity of this server run, included in control responses that carry
42
+ * cross-call state (client ids, log seq cursors). Both reset on restart, so
43
+ * a consumer that sees the generation change knows its ids and cursors are
44
+ * from a dead server and must be re-fetched.
45
+ */
46
+ generation: Date.now(),
31
47
  /**
32
48
  * The latched reload message (JSON text), replayed to late-joining clients.
33
49
  * Set by /__internal__/reload posts with `latch`, cleared by a broadcast stop.
34
50
  */
35
51
  currentReload: null as string | null,
52
+ /**
53
+ * The running bundle's sourcemap (JSON text, bundle -> .tsx sources), used
54
+ * to remap stack traces in forwarded client logs (see control.ts). Replaced
55
+ * on every reload; a reload without a map clears it so frames are never
56
+ * remapped against a stale map.
57
+ */
58
+ currentMap: null as string | null,
36
59
  sourceDir: "",
37
60
  serverUrl: "",
38
61
  stats: false,
package/src/args.ts CHANGED
@@ -46,6 +46,9 @@ export function validateArgs() {
46
46
  usage("srt bundle [options] <entry.[tsx|jsx|ts|js|srt.js|srt.bin]>")
47
47
  }
48
48
  break
49
+ case "check":
50
+ if (!source || !isSource) usage("srt check <entry.[tsx|jsx|ts|js]>")
51
+ break
49
52
  case "render":
50
53
  if (!source || !isTsx) usage("srt render <entry.[tsx|jsx]>")
51
54
  break
@@ -74,6 +77,7 @@ Commands:
74
77
  server [file] Start dev server only
75
78
  client Start solidrt-go client only
76
79
  bundle <file> Transpile TS/JS/TSX/JSX to JS or bytecode
80
+ check <file> Verify the app builds and typechecks, without writing anything
77
81
  render <file.tsx|jsx> Replay a script (optional) and render frames for video generation
78
82
  pack <file> Bundle + compile to a standalone executable (experimental)
79
83
  mcp MCP server (stdio) exposing the running dev server to coding agents
@@ -0,0 +1,12 @@
1
+ // Standalone bundler entry, spawned by the dev server (a flux process) as a
2
+ // Bun subprocess to rebuild the app on an MCP-triggered reload. flux cannot call
3
+ // Bun.build, so the server shells out to this. Params arrive as one JSON
4
+ // argument; one JSON object { code, map } goes to stdout and diagnostics to
5
+ // stderr. On a build failure it exits non-zero with an empty stdout.
6
+
7
+ import { bundleWith, type BundleOptions } from "./bundler"
8
+
9
+ let params = JSON.parse(process.argv[2] ?? "{}") as BundleOptions
10
+ let result = await bundleWith(params)
11
+ if (!result) process.exit(1)
12
+ process.stdout.write(JSON.stringify(result))
package/src/bundler.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { transformAsync } from "@babel/core"
2
2
  import jsx from "@babel/plugin-syntax-jsx"
3
3
  import ts from "@babel/preset-typescript"
4
+ import remapping from "@jridgewell/remapping"
4
5
  import solid from "babel-preset-solid"
5
6
  import { type BunPlugin, type BuildArtifact } from "bun"
6
7
  import { readFileSync } from "node:fs"
@@ -53,7 +54,7 @@ function binaryImport({ types: t }: { types: any }) {
53
54
  // build, skipping emitted asset outputs. Bun's file loader emits binary assets
54
55
  // as extra outputs; the callers that flatten outputs into a single code string
55
56
  // must not glue those raw bytes onto the program.
56
- export async function codeFromOutputs(outputs: BuildArtifact[]): Promise<string> {
57
+ async function codeFromOutputs(outputs: BuildArtifact[]): Promise<string> {
57
58
  let code = ""
58
59
  for (let o of outputs) {
59
60
  if (o.kind === "entry-point" || o.kind === "chunk") code += await o.text()
@@ -62,61 +63,106 @@ export async function codeFromOutputs(outputs: BuildArtifact[]): Promise<string>
62
63
  }
63
64
 
64
65
  // Bun build plugin that runs JSX/TSX through babel-preset-solid (universal
65
- // generate, targeting @solidrt/core) plus the TS preset.
66
- function solidPlugin(): BunPlugin {
66
+ // generate, targeting @solidrt/core) plus the TS preset. Plain .js/.ts app
67
+ // modules take the same path (solid is a no-op without JSX) so binaryImport
68
+ // can rewrite their `with { type: "binary" }` imports too; dependency code
69
+ // (node_modules) skips the babel detour and keeps Bun's native loaders.
70
+ // With `babelMaps`, each file's transform map (original -> babel output) is
71
+ // collected there, keyed by absolute path, for sourcemap composition later.
72
+ function solidPlugin(babelMaps?: Map<string, object>): BunPlugin {
67
73
  return {
68
74
  name: "bun-plugin-solid",
69
75
  setup: (build) => {
70
- build.onLoad({ filter: /\.(js|ts)x$/ }, async (args) => {
76
+ build.onLoad({ filter: /\.(js|ts)x?$/ }, async (args) => {
77
+ if (!/\.(js|ts)x$/.test(args.path) && args.path.includes("node_modules")) return
71
78
  let file = Bun.file(args.path)
72
79
  let code = await file.text()
73
80
  let transforms = await transformAsync(code, {
74
81
  filename: args.path,
82
+ sourceMaps: !!babelMaps,
75
83
  presets: [[solid, { moduleName: "@solidrt/core", generate: "universal" }], [ts]],
76
84
  plugins: [jsx, binaryImport],
77
85
  })
86
+ if (babelMaps && transforms?.map) babelMaps.set(args.path, transforms.map)
78
87
  return { contents: transforms?.code ?? "", loader: "js" }
79
88
  })
80
89
  },
81
90
  }
82
91
  }
83
92
 
84
- export async function bundle(entry = source) {
85
- let result = null
93
+ export type BundleOptions = { entry: string; devBase?: string; dev: boolean; minify: boolean }
86
94
 
87
- let devBase = state.serverUrl ?? undefined
88
- let dev = !!devBase || values.dev
89
- // Keep stdout clean when the bundle itself is written to stdout.
90
- if (!values.stdout) print(`[cli] Bundling (${dev ? "development" : "production"})`)
95
+ export type BundleResult = {
96
+ code: string
97
+ /** Composed sourcemap JSON (bundle -> original .tsx sources), dev builds only. */
98
+ map: string | null
99
+ }
100
+
101
+ // The pure bundle: every input is explicit, so it runs identically in the srt
102
+ // (Bun) process and in the standalone bundle-cli subprocess the dev server
103
+ // spawns. It never touches the ambient args/state singletons and never prints
104
+ // progress (callers own that), so its stdout stays clean for subprocess use.
105
+ export async function bundleWith(opts: BundleOptions): Promise<BundleResult | null> {
91
106
  let define: Record<string, string> = {
92
- "process.env.NODE_ENV": dev ? "development" : "production",
107
+ "process.env.NODE_ENV": opts.dev ? "development" : "production",
93
108
  }
94
- if (devBase) define.__SRT_DEV_BASE__ = devBase
109
+ if (opts.devBase) define.__SRT_DEV_BASE__ = opts.devBase
95
110
 
111
+ let babelMaps = opts.dev ? new Map<string, object>() : undefined
112
+ let result = null
96
113
  try {
97
114
  result = await Bun.build({
98
- entrypoints: [entry!],
115
+ entrypoints: [opts.entry],
99
116
  target: "browser",
100
117
  format: "esm",
101
- minify: values.minify,
118
+ minify: opts.minify,
102
119
  external: ["flux:*", "srt:*"],
103
120
  define,
104
121
  loader: { ".svg": "text" },
105
- plugins: [solidPlugin()],
122
+ sourcemap: opts.dev ? "external" : "none",
123
+ plugins: [solidPlugin(babelMaps)],
106
124
  })
107
125
  } catch (e) {
108
126
  console.error("[cli] compile error:\n", e)
109
127
  return null
110
128
  }
111
129
 
112
- if (result?.success) {
113
- return result
130
+ if (!result.success) {
131
+ for (let msg of result.logs) console.error(msg)
132
+ return null
114
133
  }
115
134
 
116
- if (result) {
117
- for (let msg of result?.logs) console.error(msg)
118
- }
119
- return null
135
+ return { code: await codeFromOutputs(result.outputs), map: await composeMap(result.outputs, babelMaps) }
136
+ }
137
+
138
+ // Compose Bun's bundle map (babel output -> bundle) with the per-file Babel
139
+ // maps (original source -> babel output) so positions point at the .tsx
140
+ // sources. Bun ignores sourcemaps in plugin onLoad contents, so this second
141
+ // hop has to happen here. Only a single-artifact build gets a map: code
142
+ // splitting is off, and concatenated artifacts would invalidate offsets.
143
+ async function composeMap(outputs: BuildArtifact[], babelMaps?: Map<string, object>): Promise<string | null> {
144
+ if (!babelMaps) return null
145
+ let js = outputs.filter((o) => o.kind === "entry-point" || o.kind === "chunk")
146
+ if (js.length !== 1 || !js[0]!.sourcemap) return null
147
+ let bunMap = JSON.parse(await js[0]!.sourcemap.text())
148
+ let composed = remapping(bunMap, (file: string) => {
149
+ // Bun writes cwd-relative source paths; the babel maps are keyed by the
150
+ // absolute path. Serve each map exactly once: remapping asks again for a
151
+ // served map's own original source, and that lookup must return null.
152
+ let abs = resolvePath(file)
153
+ let map = babelMaps.get(abs)
154
+ babelMaps.delete(abs)
155
+ return (map as any) ?? null
156
+ })
157
+ return composed.toString()
158
+ }
159
+
160
+ export async function bundle(entry = source) {
161
+ let devBase = state.serverUrl ?? undefined
162
+ let dev = !!devBase || values.dev
163
+ // Keep stdout clean when the bundle itself is written to stdout.
164
+ if (!values.stdout) print(`[cli] Bundling (${dev ? "development" : "production"})`)
165
+ return bundleWith({ entry: entry!, devBase, dev, minify: values.minify })
120
166
  }
121
167
 
122
168
  export async function bundleTo(outfile: string) {
@@ -125,7 +171,7 @@ export async function bundleTo(outfile: string) {
125
171
  console.error("Build failed")
126
172
  process.exit(1)
127
173
  }
128
- await Bun.write(outfile, await codeFromOutputs(result.outputs))
174
+ await Bun.write(outfile, result.code)
129
175
  return result
130
176
  }
131
177
 
@@ -153,7 +199,7 @@ export async function bundleSolid(): Promise<string> {
153
199
  console.error("Build failed")
154
200
  process.exit(1)
155
201
  }
156
- return codeFromOutputs(result.outputs)
202
+ return result.code
157
203
  }
158
204
 
159
205
  // Compile JS source to QuickJS bytecode via the fluxc binary.
@@ -1,5 +1,5 @@
1
1
  import { values, source, isPrebuilt } from "../args"
2
- import { bundle, bundleTo, bundleFlux, compileToBytecode, codeFromOutputs } from "../bundler"
2
+ import { bundle, bundleTo, bundleFlux, compileToBytecode } from "../bundler"
3
3
  import { resolve } from "path"
4
4
 
5
5
  // Write to stdout and resolve only once the whole payload is flushed.
@@ -55,7 +55,7 @@ export async function runBundleCommand() {
55
55
  console.error("Build failed")
56
56
  process.exit(1)
57
57
  }
58
- await writeStdout(await codeFromOutputs(result.outputs))
58
+ await writeStdout(result.code)
59
59
  process.exit()
60
60
  }
61
61
 
@@ -65,15 +65,12 @@ export async function runBundleCommand() {
65
65
  console.error("Build failed")
66
66
  process.exit(1)
67
67
  }
68
- let jsCode = await codeFromOutputs(result.outputs)
69
- await writeBytecode(jsCode, baseName + ".srt.bin")
68
+ await writeBytecode(result.code, baseName + ".srt.bin")
70
69
  process.exit()
71
70
  }
72
71
 
73
72
  let jsOutfile = baseName + ".srt.js"
74
73
  let result = await bundleTo(jsOutfile)
75
- for (let output of result.outputs) {
76
- console.log(`>> wrote ${output.size} bytes to ${jsOutfile}`)
77
- }
74
+ console.log(`>> wrote ${result.code.length} bytes to ${jsOutfile}`)
78
75
  process.exit()
79
76
  }
@@ -0,0 +1,94 @@
1
+ import { existsSync } from "node:fs"
2
+ import { dirname, join, resolve } from "node:path"
3
+ import { source } from "../args"
4
+ import { bundleWith } from "../bundler"
5
+
6
+ // srt check: verify the app without side effects. Bundles in memory (nothing
7
+ // written, so no dev-server reload fires and no build outputs land in the
8
+ // project) and typechecks with the project's own tsc, reporting only
9
+ // diagnostics in app code. @solidrt packages ship raw .ts sources, so a strict
10
+ // consumer config surfaces their internal errors too; those are counted and
11
+ // hidden, not the caller's problem to wade through.
12
+
13
+ // Walk up from the entry to the enclosing project (tsconfig.json or, failing
14
+ // that, package.json).
15
+ function findProjectRoot(entry: string): string | null {
16
+ let dir = dirname(resolve(entry))
17
+ let byConfig: string | null = null
18
+ let byPackage: string | null = null
19
+ while (true) {
20
+ if (!byConfig && existsSync(join(dir, "tsconfig.json"))) byConfig = dir
21
+ if (!byPackage && existsSync(join(dir, "package.json"))) byPackage = dir
22
+ let parent = dirname(dir)
23
+ if (parent === dir) return byConfig ?? byPackage
24
+ dir = parent
25
+ }
26
+ }
27
+
28
+ // One tsc --pretty false diagnostic: the "path(line,col): error TS...: ..."
29
+ // head line plus any indented continuation lines.
30
+ type Diagnostic = { head: string; lines: string[]; inDependencies: boolean }
31
+
32
+ function parseDiagnostics(output: string): Diagnostic[] {
33
+ let diagnostics: Diagnostic[] = []
34
+ let current: Diagnostic | null = null
35
+ for (let line of output.split("\n")) {
36
+ let head = /^(.*?)\(\d+,\d+\): (error|warning) TS\d+: /.exec(line) ?? /^(error|warning) TS\d+: /.exec(line)
37
+ if (head) {
38
+ let file = line.includes("): ") ? head[1]! : ""
39
+ current = { head: line, lines: [line], inDependencies: file.includes("node_modules") }
40
+ diagnostics.push(current)
41
+ } else if (current && line.trim() !== "") {
42
+ current.lines.push(line)
43
+ }
44
+ }
45
+ return diagnostics
46
+ }
47
+
48
+ async function typecheck(root: string): Promise<{ app: Diagnostic[]; hidden: number } | null> {
49
+ let tsc = join(root, "node_modules", ".bin", process.platform === "win32" ? "tsc.exe" : "tsc")
50
+ if (!existsSync(tsc)) {
51
+ console.warn("Typecheck skipped: no tsc in the project (add the typescript devDependency)")
52
+ return null
53
+ }
54
+ let proc = Bun.spawn([tsc, "--noEmit", "--pretty", "false"], { cwd: root, stdout: "pipe", stderr: "pipe" })
55
+ let [out, err] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()])
56
+ await proc.exited
57
+ let diagnostics = parseDiagnostics(out + err)
58
+ let app = diagnostics.filter((d) => !d.inDependencies)
59
+ return { app, hidden: diagnostics.length - app.length }
60
+ }
61
+
62
+ export async function runCheckCommand() {
63
+ let entry = source!
64
+ let failed = false
65
+
66
+ let result = await bundleWith({ entry, dev: true, minify: false })
67
+ if (!result) {
68
+ // bundleWith already printed the compile errors.
69
+ failed = true
70
+ }
71
+
72
+ let root = findProjectRoot(entry)
73
+ if (!root) {
74
+ console.warn("Typecheck skipped: no tsconfig.json or package.json above the entry")
75
+ } 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
+ }
89
+ }
90
+
91
+ if (failed) process.exit(1)
92
+ console.log("Check passed")
93
+ process.exit(0)
94
+ }
@@ -29,23 +29,48 @@ function packageName(dir: string): string {
29
29
  }
30
30
 
31
31
  const DEFAULT_TEMPLATE = "default"
32
+ const TEMPLATE_MANIFEST = "template.json"
33
+
34
+ // Each template's template.json declares which level the scaffolded app is
35
+ // written at: "core" (only @solidrt/core, no component framework) or
36
+ // "components" (built with @solidrt/components). The level decides the
37
+ // generated dependencies; the description labels the template in the picker.
38
+ interface TemplateInfo {
39
+ name: string
40
+ level: "core" | "components"
41
+ description: string
42
+ }
32
43
 
33
44
  // Templates are the directories under scaffold/templates/; each holds the files
34
- // that become the new project's src/. `default` sorts first as the starting
35
- // point, the rest alphabetically.
36
- async function listTemplates(): Promise<string[]> {
45
+ // that become the new project's src/, plus a template.json manifest. `default`
46
+ // sorts first as the starting point, the rest alphabetically.
47
+ async function listTemplates(): Promise<TemplateInfo[]> {
37
48
  let entries = await readdir(TEMPLATES_DIR, { withFileTypes: true })
38
- return entries
49
+ let names = entries
39
50
  .filter((e) => e.isDirectory())
40
51
  .map((e) => e.name)
41
52
  .sort((a, b) =>
42
53
  a === DEFAULT_TEMPLATE ? -1 : b === DEFAULT_TEMPLATE ? 1 : a.localeCompare(b),
43
54
  )
55
+ let templates: TemplateInfo[] = []
56
+ for (let name of names) {
57
+ // A missing manifest falls back to the components level: it keeps every
58
+ // dependency, so the scaffolded app works at either level.
59
+ let manifest = await readFile(join(TEMPLATES_DIR, name, TEMPLATE_MANIFEST), "utf8")
60
+ .then((raw) => JSON.parse(raw))
61
+ .catch(() => ({}))
62
+ templates.push({
63
+ name,
64
+ level: manifest.level === "core" ? "core" : "components",
65
+ description: typeof manifest.description === "string" ? manifest.description : "",
66
+ })
67
+ }
68
+ return templates
44
69
  }
45
70
 
46
71
  // Resolve which template to scaffold from: an explicit --template if valid, an
47
72
  // interactive picker on a TTY, else `default` (or the first available).
48
- async function resolveTemplate(): Promise<string> {
73
+ async function resolveTemplate(): Promise<TemplateInfo> {
49
74
  let templates = await listTemplates()
50
75
  if (templates.length === 0) {
51
76
  console.error(`!! No templates found in ${TEMPLATES_DIR}`)
@@ -53,14 +78,25 @@ async function resolveTemplate(): Promise<string> {
53
78
  }
54
79
  let chosen = values.template
55
80
  if (chosen) {
56
- if (!templates.includes(chosen)) {
57
- console.error(`!! Unknown template "${chosen}"; choose from: ${templates.join(", ")}`)
81
+ let found = templates.find((t) => t.name === chosen)
82
+ if (!found) {
83
+ let names = templates.map((t) => t.name).join(", ")
84
+ console.error(`!! Unknown template "${chosen}"; choose from: ${names}`)
58
85
  process.exit(1)
59
86
  }
60
- return chosen
87
+ return found
88
+ }
89
+ if (process.stdin.isTTY) {
90
+ let picked = await select(
91
+ "Select a template",
92
+ templates.map((t) => ({
93
+ label: t.description ? `${t.name} - ${t.description}` : t.name,
94
+ value: t.name,
95
+ })),
96
+ )
97
+ return templates.find((t) => t.name === picked)!
61
98
  }
62
- if (process.stdin.isTTY) return select("Select a template", templates)
63
- return templates.includes(DEFAULT_TEMPLATE) ? DEFAULT_TEMPLATE : templates[0]!
99
+ return templates.find((t) => t.name === DEFAULT_TEMPLATE) ?? templates[0]!
64
100
  }
65
101
 
66
102
  export async function runInitCommand() {
@@ -84,7 +120,7 @@ export async function runInitCommand() {
84
120
 
85
121
  let template = await resolveTemplate()
86
122
 
87
- console.log(`>> Scaffolding SolidRT project in ${resolve(dir)} (${template})`)
123
+ console.log(`>> Scaffolding SolidRT project in ${resolve(dir)} (${template.name})`)
88
124
  for (let { from, to } of TEMPLATE_FILES) {
89
125
  let dest = join(dir, to)
90
126
  await mkdir(dirname(dest), { recursive: true })
@@ -93,19 +129,22 @@ export async function runInitCommand() {
93
129
  }
94
130
 
95
131
  // The chosen template's files become the project's src/. Entries may be
96
- // nested directories (e.g. an asset folder), so copy recursively.
97
- let templateDir = join(TEMPLATES_DIR, template)
132
+ // nested directories (e.g. an asset folder), so copy recursively. The
133
+ // manifest describes the template rather than belonging to the app.
134
+ let templateDir = join(TEMPLATES_DIR, template.name)
98
135
  await mkdir(join(dir, "src"), { recursive: true })
99
136
  for (let file of await readdir(templateDir)) {
137
+ if (file === TEMPLATE_MANIFEST) continue
100
138
  await cp(join(templateDir, file), join(dir, "src", file), { recursive: true })
101
139
  console.log(` Write src/${file}`)
102
140
  }
103
141
 
104
142
  // The scaffold package.json carries a placeholder name; set it from the
105
- // target folder.
143
+ // target folder. A core-level app gets no component framework dependency.
106
144
  let pkgPath = join(dir, "package.json")
107
145
  let pkg = JSON.parse(await readFile(pkgPath, "utf8"))
108
146
  pkg.name = packageName(dir)
147
+ if (template.level === "core") delete pkg.dependencies["@solidrt/components"]
109
148
  await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n")
110
149
 
111
150
  // Deps are declared in scaffold/package.json (Solid peers resolve via