@solidrt/cli 0.0.50 → 0.0.52
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/AGENTS.md +79 -36
- package/README.md +86 -15
- package/agents/assets.md +46 -0
- package/agents/debugging.md +294 -0
- package/dist/console.srtapp +94123 -73
- package/dist/server.js +3936 -0
- package/package.json +11 -10
- package/src/android/docs.md +21 -0
- package/src/android/main.ts +286 -0
- package/src/{bundler.ts → bundle/bundler.ts} +163 -76
- package/src/bundle/docs.md +12 -0
- package/src/bundle/main.ts +203 -0
- package/src/check/docs.md +10 -0
- package/src/check/main.ts +85 -0
- package/src/{commands/check.ts → check/typecheck.ts} +11 -34
- package/src/client/docs.md +9 -0
- package/src/client/main.ts +33 -0
- package/src/console/docs.md +14 -0
- package/src/console/main.ts +21 -0
- package/src/demo/docs.md +27 -0
- package/src/demo/main.ts +67 -0
- package/src/init/docs.md +11 -0
- package/src/{commands/init.ts → init/main.ts} +31 -25
- package/src/init/scaffold/AGENTS.md +98 -0
- package/src/init/scaffold/package.json +23 -0
- package/{scaffold → src/init/scaffold}/templates/components/index.tsx +2 -3
- package/{scaffold → src/init/scaffold}/templates/default/index.tsx +2 -3
- package/src/lib/args.ts +194 -0
- package/src/{artifacts.ts → lib/artifacts.ts} +41 -1
- package/src/{dev-dir.ts → lib/dev-dir.ts} +10 -8
- package/src/{fonts.ts → lib/fonts.ts} +12 -26
- package/src/lib/mode.ts +77 -0
- package/src/{project.ts → lib/project.ts} +109 -61
- package/src/lib/registry.ts +120 -0
- package/src/lib/server-bundle.ts +24 -0
- package/src/lib/usage.ts +117 -0
- package/src/lib/util.ts +36 -0
- package/src/main.ts +109 -31
- package/src/mcp/docs.md +22 -0
- package/src/mcp/main.ts +719 -0
- package/src/pack/docs.md +18 -0
- package/src/{pack-folder.ts → pack/layout.ts} +13 -22
- package/src/pack/main.ts +86 -0
- package/src/pack/trailer.ts +97 -0
- package/src/render/docs.md +19 -0
- package/src/render/main.ts +49 -0
- package/src/server/args.ts +126 -0
- package/src/server/binaries.ts +47 -0
- package/src/server/config.ts +54 -0
- package/{server → src/server}/control.ts +246 -78
- package/src/server/docs.md +51 -0
- package/src/server/line-editor.ts +200 -0
- package/src/server/main.ts +473 -0
- package/src/server/mode.ts +92 -0
- package/src/server/rebuild.ts +90 -0
- package/src/server/registry.ts +138 -0
- package/src/server/remap.ts +60 -0
- package/src/server/repl.ts +223 -0
- package/src/server/state.ts +54 -0
- package/{server → src/server}/tsconfig.json +1 -1
- package/{server → src/server}/tunnel.ts +6 -6
- package/src/server/watcher.ts +121 -0
- package/src/tool/main.ts +70 -0
- package/src/types/bundle.d.ts +24 -0
- package/src/types/control.d.ts +90 -0
- package/src/types/registry.d.ts +16 -0
- package/scaffold/AGENTS.md +0 -630
- package/scaffold/package.json +0 -22
- package/scaffold/templates/components/icon.tsx +0 -48
- package/scaffold/templates/default/icon.tsx +0 -48
- package/server/main.ts +0 -308
- package/server/rebuild.ts +0 -68
- package/server/remap.ts +0 -47
- package/server/state.ts +0 -93
- package/src/args.ts +0 -212
- package/src/bundle-cli.ts +0 -13
- package/src/commands/bundle.ts +0 -76
- package/src/commands/client.ts +0 -34
- package/src/commands/mcp.ts +0 -603
- package/src/commands/pack.ts +0 -65
- package/src/commands/render.ts +0 -24
- package/src/commands/server.ts +0 -73
- package/src/dev-android.ts +0 -176
- package/src/dev-client.ts +0 -29
- package/src/dev-server.ts +0 -302
- package/src/packer.ts +0 -103
- package/src/repl.ts +0 -233
- package/src/util.ts +0 -121
- package/src/watcher.ts +0 -69
- /package/src/{untyped-deps.d.ts → bundle/untyped-deps.d.ts} +0 -0
- /package/src/{prompt.ts → init/prompt.ts} +0 -0
- /package/{scaffold → src/init/scaffold}/gitignore +0 -0
- /package/{scaffold → src/init/scaffold}/icon.svg +0 -0
- /package/{scaffold → src/init/scaffold}/mcp.json +0 -0
- /package/{scaffold → src/init/scaffold}/tsconfig.json +0 -0
- /package/{server → src/server}/cache.ts +0 -0
- /package/{server → src/server}/proxy.ts +0 -0
- /package/{server → src/server}/qr.ts +0 -0
|
@@ -4,13 +4,15 @@ import ts from "@babel/preset-typescript"
|
|
|
4
4
|
import remapping from "@jridgewell/remapping"
|
|
5
5
|
import solid from "babel-preset-solid"
|
|
6
6
|
import { type BunPlugin, type BuildArtifact } from "bun"
|
|
7
|
-
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"
|
|
8
8
|
import { dirname, join, relative, resolve as resolvePath, sep } from "node:path"
|
|
9
|
-
import { values
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
9
|
+
import { values } from "../lib/args"
|
|
10
|
+
import type { Mode } from "../lib/mode"
|
|
11
|
+
import { requireBinary } from "../lib/util"
|
|
12
|
+
import type { BundleOutput } from "../types/bundle"
|
|
13
|
+
import { buildManifest, manifestAssetFor, type ManifestAsset } from "../lib/project"
|
|
12
14
|
|
|
13
|
-
// Babel plugin: rewrite `import data from "
|
|
15
|
+
// Babel plugin: rewrite `import data from "../x" with { type: "binary" }` into an
|
|
14
16
|
// inline Uint8Array of the file's bytes, and `with { type: "text" }` into an
|
|
15
17
|
// inline string of its UTF-8 contents. The import attribute is invisible to
|
|
16
18
|
// Bun's bundler and its plugins in this Bun version, so we handle it here in the
|
|
@@ -21,8 +23,10 @@ import { buildManifest, manifestAssetFor } from "./project"
|
|
|
21
23
|
// attribute-free path later. Both attributes work on any extension, so shader
|
|
22
24
|
// and other text sources inline by attribute the same way bytes do; `.svg` is
|
|
23
25
|
// additionally text-loaded without an attribute (see Bun's `loader` below).
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
// Inlined files leave no trace in the module graph, so the plugin records
|
|
27
|
+
// each one in `inputs` (see `BundleOutput.inputs`).
|
|
28
|
+
function inlineImport(inputs: Set<string>) {
|
|
29
|
+
return ({ types: t }: { types: any }) => ({
|
|
26
30
|
visitor: {
|
|
27
31
|
ImportDeclaration(path: any, pluginState: any) {
|
|
28
32
|
let attrs = path.node.attributes ?? path.node.assertions
|
|
@@ -32,12 +36,13 @@ function inlineImport({ types: t }: { types: any }) {
|
|
|
32
36
|
let def = path.node.specifiers.find((s: any) => s.type === "ImportDefaultSpecifier")
|
|
33
37
|
if (!def) {
|
|
34
38
|
throw path.buildCodeFrameError(
|
|
35
|
-
`A ${kind} import needs a default import: import data from "
|
|
39
|
+
`A ${kind} import needs a default import: import data from "../file" with { type: "${kind}" }`,
|
|
36
40
|
)
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
let importer = pluginState.file.opts.filename as string
|
|
40
44
|
let abs = resolvePath(dirname(importer), path.node.source.value)
|
|
45
|
+
inputs.add(abs)
|
|
41
46
|
|
|
42
47
|
// text: var <local> = "<contents>"
|
|
43
48
|
// binary: var <local> = Uint8Array.from(atob("<b64>"), c => c.charCodeAt(0))
|
|
@@ -56,7 +61,7 @@ function inlineImport({ types: t }: { types: any }) {
|
|
|
56
61
|
path.replaceWith(t.variableDeclaration("var", [t.variableDeclarator(t.identifier(def.local.name), expr)]))
|
|
57
62
|
},
|
|
58
63
|
},
|
|
59
|
-
}
|
|
64
|
+
})
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
// Concatenate only the JS outputs (entry point plus any code-split chunks) of a
|
|
@@ -84,25 +89,27 @@ async function codeFromOutputs(outputs: BuildArtifact[]): Promise<string> {
|
|
|
84
89
|
// collected there, keyed by absolute path, for sourcemap composition later.
|
|
85
90
|
// `isolateEntry` is the one "use isolate" module this build may load (its own
|
|
86
91
|
// entry); loading any other one means a by-value import of an isolate module,
|
|
87
|
-
// which is a build error (see isolate modules below).
|
|
88
|
-
|
|
92
|
+
// which is a build error (see isolate modules below). Every file the plugin
|
|
93
|
+
// loads or inlines is recorded in `inputs`.
|
|
94
|
+
function solidPlugin(inputs: Set<string>, babelMaps?: Map<string, object>, isolateEntry?: string): BunPlugin {
|
|
89
95
|
return {
|
|
90
96
|
name: "bun-plugin-solid",
|
|
91
97
|
setup: (build) => {
|
|
92
98
|
build.onLoad({ filter: /\.(js|ts)x?$/ }, async (args) => {
|
|
93
99
|
if (!/\.(js|ts)x$/.test(args.path) && args.path.includes("node_modules")) return
|
|
100
|
+
inputs.add(args.path)
|
|
94
101
|
let file = Bun.file(args.path)
|
|
95
102
|
let code = await file.text()
|
|
96
103
|
if (args.path !== isolateEntry && hasIsolateDirective(code)) {
|
|
97
104
|
throw new Error(
|
|
98
|
-
`${args.path} is a "use isolate" module: import its types only (import type * as W from "
|
|
105
|
+
`${args.path} is a "use isolate" module: import its types only (import type * as W from "../...") and call it through isolate() from flux:isolate`,
|
|
99
106
|
)
|
|
100
107
|
}
|
|
101
108
|
let transforms = await transformAsync(code, {
|
|
102
109
|
filename: args.path,
|
|
103
110
|
sourceMaps: !!babelMaps,
|
|
104
111
|
presets: [[solid, { moduleName: "@solidrt/core", generate: "universal" }], [ts]],
|
|
105
|
-
plugins: [jsx, inlineImport],
|
|
112
|
+
plugins: [jsx, inlineImport(inputs)],
|
|
106
113
|
})
|
|
107
114
|
if (babelMaps && transforms?.map) babelMaps.set(args.path, transforms.map)
|
|
108
115
|
return { contents: transforms?.code ?? "", loader: "js" }
|
|
@@ -129,26 +136,39 @@ export function hasIsolateDirective(code: string): boolean {
|
|
|
129
136
|
|
|
130
137
|
let SKIP_DIRS = new Set(["node_modules", "dist"])
|
|
131
138
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Depth-first files under `root`, visited as (absolute path, forward-slash
|
|
141
|
+
* path relative to root). Dotfiles and `skipDirs` are skipped; a missing
|
|
142
|
+
* root visits nothing.
|
|
143
|
+
*/
|
|
144
|
+
export function walkFiles(root: string, visit: (abs: string, rel: string) => void, skipDirs?: Set<string>) {
|
|
145
|
+
if (!existsSync(root)) return
|
|
137
146
|
let walk = (dir: string) => {
|
|
138
147
|
for (let entry of readdirSync(dir, { withFileTypes: true })) {
|
|
139
|
-
if (entry.name.startsWith(".") ||
|
|
148
|
+
if (entry.name.startsWith(".") || skipDirs?.has(entry.name)) continue
|
|
140
149
|
let abs = join(dir, entry.name)
|
|
141
|
-
if (entry.isDirectory())
|
|
142
|
-
|
|
143
|
-
} else if (entry.isFile() && /\.(js|ts)x?$/.test(entry.name) && !entry.name.endsWith(".d.ts")) {
|
|
144
|
-
if (hasIsolateDirective(readFileSync(abs, "utf8"))) {
|
|
145
|
-
let id = relative(root, abs).split(sep).join("/").replace(/\.(js|ts)x?$/, "")
|
|
146
|
-
out.push({ id, path: abs })
|
|
147
|
-
}
|
|
148
|
-
}
|
|
150
|
+
if (entry.isDirectory()) walk(abs)
|
|
151
|
+
else if (entry.isFile()) visit(abs, relative(root, abs).split(sep).join("/"))
|
|
149
152
|
}
|
|
150
153
|
}
|
|
151
154
|
walk(root)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export type IsolateModule = { id: string; path: string }
|
|
158
|
+
|
|
159
|
+
/** Every "use isolate" module under `root`, in id order. */
|
|
160
|
+
export function findIsolateModules(root: string): IsolateModule[] {
|
|
161
|
+
let out: IsolateModule[] = []
|
|
162
|
+
walkFiles(
|
|
163
|
+
root,
|
|
164
|
+
(abs, rel) => {
|
|
165
|
+
if (!/\.(js|ts)x?$/.test(rel) || rel.endsWith(".d.ts")) return
|
|
166
|
+
if (hasIsolateDirective(readFileSync(abs, "utf8"))) {
|
|
167
|
+
out.push({ id: rel.replace(/\.(js|ts)x?$/, ""), path: abs })
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
SKIP_DIRS,
|
|
171
|
+
)
|
|
152
172
|
out.sort((a, b) => (a.id < b.id ? -1 : 1))
|
|
153
173
|
return out
|
|
154
174
|
}
|
|
@@ -158,20 +178,28 @@ export function isolateAssetPath(id: string, ext: "js" | "bin"): string {
|
|
|
158
178
|
return `isolates/${id}.${ext}`
|
|
159
179
|
}
|
|
160
180
|
|
|
161
|
-
|
|
181
|
+
// `project` is the project root (mode.ts decides it, never a search), or
|
|
182
|
+
// null for a file on its own: no assets in the manifest and no isolate
|
|
183
|
+
// modules (isolates are a project feature).
|
|
184
|
+
export type BundleOptions = { entry: string; dev: boolean; minify: boolean; project: string | null }
|
|
185
|
+
|
|
186
|
+
// The `srt bundle --json` stdout contract doubles as the in-process result.
|
|
187
|
+
export type BundleResult = BundleOutput
|
|
162
188
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
189
|
+
/**
|
|
190
|
+
* The bundle's sourcemaps keyed by the module name stack frames cite ("main"
|
|
191
|
+
* for the app, the isolate id for each isolate), for the server's log remap.
|
|
192
|
+
* Null when the build carries no maps (production builds).
|
|
193
|
+
*/
|
|
194
|
+
export function bundleMaps(result: BundleResult): Record<string, string> | null {
|
|
195
|
+
let maps: Record<string, string> = {}
|
|
196
|
+
if (result.map) maps.main = result.map
|
|
197
|
+
for (let i of result.isolates) if (i.map) maps[i.id] = i.map
|
|
198
|
+
return Object.keys(maps).length ? maps : null
|
|
171
199
|
}
|
|
172
200
|
|
|
173
201
|
// The pure bundle: every input is explicit, so it runs identically in the srt
|
|
174
|
-
// (Bun) process and in the
|
|
202
|
+
// (Bun) process and in the `srt bundle --json` subprocess the dev server
|
|
175
203
|
// spawns. It never touches the ambient args/state singletons and never prints
|
|
176
204
|
// progress (callers own that), so its stdout stays clean for subprocess use.
|
|
177
205
|
export async function bundleWith(opts: BundleOptions): Promise<BundleResult | null> {
|
|
@@ -185,12 +213,15 @@ export async function bundleWith(opts: BundleOptions): Promise<BundleResult | nu
|
|
|
185
213
|
"import.meta.env.DEV": opts.dev ? "true" : "false",
|
|
186
214
|
"process.env.NODE_ENV": opts.dev ? '"development"' : '"production"',
|
|
187
215
|
}
|
|
188
|
-
if (opts.devBase) define.__SRT_DEV_BASE__ = opts.devBase
|
|
189
216
|
|
|
190
217
|
// One Bun.build per entry: the app, then each isolate module as its own
|
|
191
218
|
// self-contained bundle (splitting is off, so a helper both import gets
|
|
192
|
-
// duplicated rather than shared).
|
|
193
|
-
// sourcemap
|
|
219
|
+
// duplicated rather than shared). In dev every build gets a composed
|
|
220
|
+
// sourcemap, keyed downstream by its module name.
|
|
221
|
+
// Every file the bundle depends on (BundleOutput.inputs): what the plugin
|
|
222
|
+
// loaded or inlined, plus every module in the build's sourcemap, which is
|
|
223
|
+
// where dependency code (node_modules, workspace packages) shows up.
|
|
224
|
+
let inputs = new Set<string>()
|
|
194
225
|
let build = async (entry: string, babelMaps?: Map<string, object>, isolateEntry?: string) => {
|
|
195
226
|
let result = null
|
|
196
227
|
try {
|
|
@@ -203,7 +234,7 @@ export async function bundleWith(opts: BundleOptions): Promise<BundleResult | nu
|
|
|
203
234
|
define,
|
|
204
235
|
loader: { ".svg": "text" },
|
|
205
236
|
sourcemap: babelMaps ? "external" : "none",
|
|
206
|
-
plugins: [solidPlugin(babelMaps, isolateEntry)],
|
|
237
|
+
plugins: [solidPlugin(inputs, babelMaps, isolateEntry)],
|
|
207
238
|
})
|
|
208
239
|
} catch (e) {
|
|
209
240
|
console.error("[cli] compile error:\n", e)
|
|
@@ -213,6 +244,7 @@ export async function bundleWith(opts: BundleOptions): Promise<BundleResult | nu
|
|
|
213
244
|
for (let msg of result.logs) console.error(msg)
|
|
214
245
|
return null
|
|
215
246
|
}
|
|
247
|
+
for (let source of await mapSources(result.outputs)) inputs.add(source)
|
|
216
248
|
return result
|
|
217
249
|
}
|
|
218
250
|
|
|
@@ -221,26 +253,56 @@ export async function bundleWith(opts: BundleOptions): Promise<BundleResult | nu
|
|
|
221
253
|
if (!main) return null
|
|
222
254
|
let code = await codeFromOutputs(main.outputs)
|
|
223
255
|
|
|
224
|
-
let isolates: { id: string; code: string }[] = []
|
|
225
|
-
|
|
226
|
-
|
|
256
|
+
let isolates: { id: string; code: string; map: string | null }[] = []
|
|
257
|
+
let modules = opts.project === null ? [] : findIsolateModules(dirname(resolvePath(opts.entry)))
|
|
258
|
+
for (let module of modules) {
|
|
259
|
+
let moduleMaps = opts.dev ? new Map<string, object>() : undefined
|
|
260
|
+
let result = await build(module.path, moduleMaps, module.path)
|
|
227
261
|
if (!result) return null
|
|
228
|
-
isolates.push({
|
|
262
|
+
isolates.push({
|
|
263
|
+
id: module.id,
|
|
264
|
+
code: await codeFromOutputs(result.outputs),
|
|
265
|
+
map: await composeMap(result.outputs, moduleMaps),
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// The project files the manifest and the build read: package.json (app
|
|
270
|
+
// identity, icon, fonts) and tsconfig.json (resolution, JSX).
|
|
271
|
+
if (opts.project !== null) {
|
|
272
|
+
for (let name of ["package.json", "tsconfig.json"]) {
|
|
273
|
+
let abs = join(opts.project, name)
|
|
274
|
+
if (existsSync(abs)) inputs.add(abs)
|
|
275
|
+
}
|
|
229
276
|
}
|
|
230
277
|
|
|
231
|
-
let extra = isolates.map((i) => manifestAssetFor(isolateAssetPath(i.id, "js"), Buffer.from(i.code, "utf8")))
|
|
232
278
|
return {
|
|
233
279
|
code,
|
|
234
280
|
map: await composeMap(main.outputs, babelMaps),
|
|
235
|
-
manifest: buildManifest(code, opts.entry,
|
|
281
|
+
manifest: buildManifest(code, opts.entry, isolateManifestAssets(isolates), opts.project),
|
|
236
282
|
isolates,
|
|
283
|
+
inputs: [...inputs].sort(),
|
|
237
284
|
}
|
|
238
285
|
}
|
|
239
286
|
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
287
|
+
// The source files a build's sourcemap cites, as absolute paths (Bun writes
|
|
288
|
+
// them cwd-relative). Empty for a build without a map.
|
|
289
|
+
async function mapSources(outputs: BuildArtifact[]): Promise<string[]> {
|
|
290
|
+
let out: string[] = []
|
|
291
|
+
for (let o of outputs) {
|
|
292
|
+
if ((o.kind !== "entry-point" && o.kind !== "chunk") || !o.sourcemap) continue
|
|
293
|
+
let map = JSON.parse(await o.sourcemap.text())
|
|
294
|
+
for (let source of map.sources ?? []) out.push(resolvePath(source))
|
|
295
|
+
}
|
|
296
|
+
return out
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** The manifest assets for a set of isolate bundles (dev form: isolates/<id>.js). */
|
|
300
|
+
export function isolateManifestAssets(isolates: { id: string; code: string }[]): ManifestAsset[] {
|
|
301
|
+
return isolates.map((i) => manifestAssetFor(isolateAssetPath(i.id, "js"), Buffer.from(i.code, "utf8")))
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Write isolate bundles as `<dir>/<id>.js`, the shape the manifest lists and
|
|
305
|
+
// a prebuilt bundle's sibling isolates/ dir carries.
|
|
244
306
|
export function writeIsolates(dir: string, isolates: { id: string; code: string }[]) {
|
|
245
307
|
for (let i of isolates) {
|
|
246
308
|
let file = join(dir, `${i.id}.js`)
|
|
@@ -271,30 +333,52 @@ async function composeMap(outputs: BuildArtifact[], babelMaps?: Map<string, obje
|
|
|
271
333
|
return composed.toString()
|
|
272
334
|
}
|
|
273
335
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
336
|
+
// The one-shot commands' bundle (bundle, pack, render): production unless
|
|
337
|
+
// --dev, on what the cwd and the argument decide (mode.ts).
|
|
338
|
+
export async function bundle(mode: Mode) {
|
|
339
|
+
let dev = values.dev
|
|
277
340
|
// Keep stdout clean when the bundle itself is written to stdout.
|
|
278
|
-
if (!values.stdout)
|
|
279
|
-
|
|
280
|
-
// With a server running, its /isolates/ route serves what we write here.
|
|
281
|
-
if (result && devBase) writeIsolates(devIsolatesDir(state.projectDir), result.isolates)
|
|
282
|
-
return result
|
|
341
|
+
if (!values.stdout) console.log(`[cli] Bundling (${dev ? "development" : "production"})`)
|
|
342
|
+
return bundleWith({ entry: mode.entry, dev, minify: values.minify, project: mode.projectDir })
|
|
283
343
|
}
|
|
284
344
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
345
|
+
// A flux entry's isolate modules: everything under its isolates/ dir, id =
|
|
346
|
+
// the path relative to that dir without extension. Standalone flux resolves
|
|
347
|
+
// isolates by location, not directive - module <id> is
|
|
348
|
+
// <entry dir>/isolates/<id>.bin or .js - so this is the discovery for
|
|
349
|
+
// bundling and packing flux scripts (which also lets a worker be .ts, unlike
|
|
350
|
+
// running from source).
|
|
351
|
+
export function findFluxIsolates(entryDir: string): IsolateModule[] {
|
|
352
|
+
let out: IsolateModule[] = []
|
|
353
|
+
walkFiles(join(entryDir, "isolates"), (abs, rel) => {
|
|
354
|
+
if (/\.[jt]s$/.test(rel) && !rel.endsWith(".d.ts")) {
|
|
355
|
+
out.push({ id: rel.replace(/\.[jt]s$/, ""), path: abs })
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
out.sort((a, b) => (a.id < b.id ? -1 : 1))
|
|
359
|
+
return out
|
|
288
360
|
}
|
|
289
361
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
return
|
|
362
|
+
// A bundle cannot carry its isolate bundles inside itself, so they travel in
|
|
363
|
+
// the isolates/ dir next to it: `<dir>/isolates/<id>.js` (or `.bin`,
|
|
364
|
+
// compiled) beside the bundle file - the shape the flux runtime and an
|
|
365
|
+
// installed version dir resolve. Writes are confined to bundle-owned output
|
|
366
|
+
// dirs (the ensureOutDir rule in the bundle command); loads read the dir
|
|
367
|
+
// from wherever the bundle sits.
|
|
368
|
+
export function bundleIsolatesDir(bundlePath: string): string {
|
|
369
|
+
return join(dirname(bundlePath), "isolates")
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** A prebuilt bundle's isolate bundles, read back from its sibling dir; no dir means none. */
|
|
373
|
+
export function readPrebuiltIsolates(bundlePath: string): { id: string; code: string }[] {
|
|
374
|
+
let out: { id: string; code: string }[] = []
|
|
375
|
+
walkFiles(bundleIsolatesDir(resolvePath(bundlePath)), (abs, rel) => {
|
|
376
|
+
if (rel.endsWith(".js")) {
|
|
377
|
+
out.push({ id: rel.replace(/\.js$/, ""), code: readFileSync(abs, "utf8") })
|
|
378
|
+
}
|
|
379
|
+
})
|
|
380
|
+
out.sort((a, b) => (a.id < b.id ? -1 : 1))
|
|
381
|
+
return out
|
|
298
382
|
}
|
|
299
383
|
|
|
300
384
|
// Bundle for the bare Flux runtime: no Solid plugin, flux: modules stay external.
|
|
@@ -314,9 +398,10 @@ export async function bundleFlux(entry: string): Promise<string> {
|
|
|
314
398
|
return codeFromOutputs(result.outputs)
|
|
315
399
|
}
|
|
316
400
|
|
|
317
|
-
// Bundle for the SolidRT runtime via the standard Solid-aware bundler
|
|
318
|
-
|
|
319
|
-
|
|
401
|
+
// Bundle for the SolidRT runtime via the standard Solid-aware bundler, or
|
|
402
|
+
// exit the command on a failed build.
|
|
403
|
+
export async function bundleSolid(mode: Mode): Promise<BundleResult> {
|
|
404
|
+
let result = await bundle(mode)
|
|
320
405
|
if (!result) {
|
|
321
406
|
console.error("Build failed")
|
|
322
407
|
process.exit(1)
|
|
@@ -324,10 +409,12 @@ export async function bundleSolid(): Promise<BundleResult> {
|
|
|
324
409
|
return result
|
|
325
410
|
}
|
|
326
411
|
|
|
327
|
-
// Compile JS source to QuickJS bytecode via the fluxc binary.
|
|
328
|
-
|
|
412
|
+
// Compile JS source to QuickJS bytecode via the fluxc binary. `moduleName` is
|
|
413
|
+
// what stack frames cite at runtime: "main" for the entry, the isolate id for
|
|
414
|
+
// an isolate bundle.
|
|
415
|
+
export async function compileToBytecode(jsCode: string, moduleName = "main"): Promise<Buffer> {
|
|
329
416
|
let compiler = requireBinary("fluxc")
|
|
330
|
-
let proc = Bun.spawn([compiler], {
|
|
417
|
+
let proc = Bun.spawn([compiler, moduleName], {
|
|
331
418
|
stdin: new Blob([jsCode]),
|
|
332
419
|
stdout: "pipe",
|
|
333
420
|
stderr: "inherit",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# srt bundle
|
|
2
|
+
|
|
3
|
+
{{ usage bundle }}
|
|
4
|
+
|
|
5
|
+
Writes `dist/bundle/` (or `--output <dir>`): `<name>.srt.js` plus the app's
|
|
6
|
+
isolate modules as `isolates/<id>.js`, or bytecode with `--compile`. Move
|
|
7
|
+
the dir, not the bare file: a bundle loaded without its isolates/ dir loses
|
|
8
|
+
them.
|
|
9
|
+
|
|
10
|
+
`--flux` targets the bare [Flux runtime](/runtime/) instead of a SolidRT
|
|
11
|
+
app, for scripts and servers with no UI. `--json` is the dev server's
|
|
12
|
+
rebuild contract, one JSON object on stdout.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { values, source, isPrebuilt } from "../lib/args"
|
|
2
|
+
import {
|
|
3
|
+
bundleFlux,
|
|
4
|
+
bundleSolid,
|
|
5
|
+
bundleWith,
|
|
6
|
+
compileToBytecode,
|
|
7
|
+
findFluxIsolates,
|
|
8
|
+
isolateManifestAssets,
|
|
9
|
+
readPrebuiltIsolates,
|
|
10
|
+
walkFiles,
|
|
11
|
+
writeIsolates,
|
|
12
|
+
} from "./bundler"
|
|
13
|
+
import { resolveMode } from "../lib/mode"
|
|
14
|
+
import { buildManifest } from "../lib/project"
|
|
15
|
+
import type { BundleOutput } from "../types/bundle"
|
|
16
|
+
import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs"
|
|
17
|
+
import { basename, dirname, join, resolve } from "node:path"
|
|
18
|
+
|
|
19
|
+
// Write to stdout and resolve only once the whole payload is flushed.
|
|
20
|
+
// process.stdout.write to a pipe is async and applies backpressure; the
|
|
21
|
+
// callback fires after every byte is drained, so it is safe to exit after.
|
|
22
|
+
function writeStdout(data: string): Promise<void> {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
process.stdout.write(data, (err) => (err ? reject(err) : resolve()))
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The bundle output dir (okf/backlog/build-output-dirs.md): the bundle flow's
|
|
29
|
+
// subdir of the build root, or an explicit --output dir. The build root is
|
|
30
|
+
// the cwd: a project command runs in its root (mode.ts), and a file on its
|
|
31
|
+
// own builds where it is run from. Only reused when it is empty or already a
|
|
32
|
+
// bundle output (a *.srt.* or *.flux.* bundle at top level) - the
|
|
33
|
+
// writePackFolder rule - so it never writes into an unrelated directory.
|
|
34
|
+
function ensureOutDir(defaultDir = join("dist", "bundle")): string {
|
|
35
|
+
let outDir = values.output ?? defaultDir
|
|
36
|
+
let existing = existsSync(outDir) ? readdirSync(outDir) : null
|
|
37
|
+
if (existing && existing.length > 0 && !existing.some((name) => /\.(srt|flux)\.(js|bin)$/.test(name))) {
|
|
38
|
+
console.error(`${resolve(outDir)} exists and is not a bundle output; choose another --output`)
|
|
39
|
+
process.exit(1)
|
|
40
|
+
}
|
|
41
|
+
mkdirSync(outDir, { recursive: true })
|
|
42
|
+
return outDir
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Clear one form's files from the output's isolates/ dir before rewriting it,
|
|
46
|
+
// so removed modules cannot go stale. The dir is shared by a bundle's .js and
|
|
47
|
+
// .bin forms, so only the form being rewritten is cleared - a --compile must
|
|
48
|
+
// not delete the .js set the .js bundle pairs with, nor the reverse.
|
|
49
|
+
function clearIsolates(dir: string, ext: ".js" | ".bin") {
|
|
50
|
+
walkFiles(dir, (abs) => {
|
|
51
|
+
if (abs.endsWith(ext)) rmSync(abs)
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Compile one isolate bundle to `<dir>/<id>.bin` (module name = its id, for
|
|
56
|
+
// stack attribution).
|
|
57
|
+
async function writeIsolateBytecode(dir: string, isolate: { id: string; code: string }) {
|
|
58
|
+
let outfile = join(dir, isolate.id + ".bin")
|
|
59
|
+
mkdirSync(dirname(outfile), { recursive: true })
|
|
60
|
+
await Bun.write(outfile, await compileToBytecode(isolate.code, isolate.id))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Compile JS to a bytecode file and report its size.
|
|
64
|
+
async function writeBytecode(jsCode: string, outfile: string) {
|
|
65
|
+
let bytecode = await compileToBytecode(jsCode)
|
|
66
|
+
await Bun.write(outfile, bytecode)
|
|
67
|
+
let binSize = (await Bun.file(outfile).stat()).size
|
|
68
|
+
console.log(`>> wrote ${binSize} bytes to ${outfile}`)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function main() {
|
|
72
|
+
if (values.flux) {
|
|
73
|
+
let entry = resolve(source!)
|
|
74
|
+
let name = basename(entry).replace(/\.[jt]s$/, "")
|
|
75
|
+
let jsCode = await bundleFlux(entry)
|
|
76
|
+
// Standalone flux resolves isolates by location, not directive: module
|
|
77
|
+
// <id> is <entry dir>/isolates/<id>.js. Bundling keeps that shape - every
|
|
78
|
+
// module under the entry's isolates/ dir is built bare like the entry
|
|
79
|
+
// (which also lets a worker be .ts, unlike running from source) and lands
|
|
80
|
+
// as isolates/<id>.js next to the bundle.
|
|
81
|
+
let isolateModules = findFluxIsolates(dirname(entry))
|
|
82
|
+
|
|
83
|
+
if (values.stdout) {
|
|
84
|
+
if (isolateModules.length) {
|
|
85
|
+
console.error("[cli] Warning: this script has isolate modules; --stdout carries only the main bundle")
|
|
86
|
+
}
|
|
87
|
+
await writeStdout(jsCode)
|
|
88
|
+
process.exit()
|
|
89
|
+
}
|
|
90
|
+
let outDir = ensureOutDir()
|
|
91
|
+
if (values.compile) {
|
|
92
|
+
await writeBytecode(jsCode, join(outDir, name + ".flux.bin"))
|
|
93
|
+
} else {
|
|
94
|
+
let outfile = join(outDir, name + ".flux.js")
|
|
95
|
+
await Bun.write(outfile, jsCode)
|
|
96
|
+
console.log(`>> wrote ${jsCode.length} bytes to ${outfile}`)
|
|
97
|
+
}
|
|
98
|
+
// Isolates follow the main bundle's form: source beside a .flux.js,
|
|
99
|
+
// bytecode beside a .flux.bin (the flux resolver reads .bin first).
|
|
100
|
+
let isolatesDir = join(outDir, "isolates")
|
|
101
|
+
if (values.compile) {
|
|
102
|
+
clearIsolates(isolatesDir, ".bin")
|
|
103
|
+
for (let module of isolateModules) {
|
|
104
|
+
await writeIsolateBytecode(isolatesDir, { id: module.id, code: await bundleFlux(module.path) })
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
clearIsolates(isolatesDir, ".js")
|
|
108
|
+
for (let module of isolateModules) {
|
|
109
|
+
let file = join(isolatesDir, module.id + ".js")
|
|
110
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
111
|
+
await Bun.write(file, await bundleFlux(module.path))
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
process.exit()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --json: the dev server's rebuild (server/rebuild.ts spawns it in the
|
|
118
|
+
// project root or the entry's directory with the entry and --project or
|
|
119
|
+
// --file, so the mode resolves the same). One BundleOutput object on
|
|
120
|
+
// stdout (types/bundle.d.ts), diagnostics on stderr, exit 1 with an empty
|
|
121
|
+
// stdout on a build failure. A prebuilt .srt.js is read as-is with its
|
|
122
|
+
// sibling isolate bundles.
|
|
123
|
+
if (values.json) {
|
|
124
|
+
let mode = resolveMode()
|
|
125
|
+
let result: BundleOutput | null
|
|
126
|
+
if (mode.entry.endsWith(".srt.js")) {
|
|
127
|
+
let code = await Bun.file(mode.entry).text()
|
|
128
|
+
let isolates = readPrebuiltIsolates(mode.entry).map((i) => ({ ...i, map: null }))
|
|
129
|
+
result = {
|
|
130
|
+
code,
|
|
131
|
+
map: null,
|
|
132
|
+
manifest: buildManifest(code, mode.entry, isolateManifestAssets(isolates), mode.projectDir),
|
|
133
|
+
isolates,
|
|
134
|
+
inputs: [mode.entry],
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
result = await bundleWith({
|
|
138
|
+
entry: mode.entry,
|
|
139
|
+
project: mode.projectDir,
|
|
140
|
+
dev: values.dev,
|
|
141
|
+
minify: values.minify,
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
if (!result) process.exit(1)
|
|
145
|
+
await writeStdout(JSON.stringify(result))
|
|
146
|
+
process.exit()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// A prebuilt .srt.js (validateArgs admits no other prebuilt form) is
|
|
150
|
+
// compiled to bytecode: the only step left, so --compile is implied. The
|
|
151
|
+
// output lands in the bundle's own dir unless --output says otherwise; its
|
|
152
|
+
// isolate bundles compile along into the output's isolates/ (the ids match,
|
|
153
|
+
// the extension picks the form).
|
|
154
|
+
if (isPrebuilt) {
|
|
155
|
+
let jsFile = resolve(source!)
|
|
156
|
+
if (!existsSync(jsFile)) {
|
|
157
|
+
console.error(`Entry not found: ${source}`)
|
|
158
|
+
process.exit(1)
|
|
159
|
+
}
|
|
160
|
+
let outDir = ensureOutDir(dirname(jsFile))
|
|
161
|
+
await writeBytecode(await Bun.file(jsFile).text(), join(outDir, basename(jsFile).replace(/\.js$/, ".bin")))
|
|
162
|
+
let isolatesDir = join(outDir, "isolates")
|
|
163
|
+
clearIsolates(isolatesDir, ".bin")
|
|
164
|
+
for (let isolate of readPrebuiltIsolates(jsFile)) {
|
|
165
|
+
await writeIsolateBytecode(isolatesDir, isolate)
|
|
166
|
+
}
|
|
167
|
+
process.exit()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let mode = resolveMode()
|
|
171
|
+
let entry = mode.entry
|
|
172
|
+
let name = basename(entry).replace(/\.[jt]sx?$/, "")
|
|
173
|
+
|
|
174
|
+
if (values.stdout) {
|
|
175
|
+
let result = await bundleSolid(mode)
|
|
176
|
+
if (result.isolates.length) {
|
|
177
|
+
console.error("[cli] Warning: this app has isolate modules; --stdout carries only the main bundle")
|
|
178
|
+
}
|
|
179
|
+
await writeStdout(result.code)
|
|
180
|
+
process.exit()
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let outDir = ensureOutDir()
|
|
184
|
+
let isolatesDir = join(outDir, "isolates")
|
|
185
|
+
|
|
186
|
+
if (values.compile) {
|
|
187
|
+
let result = await bundleSolid(mode)
|
|
188
|
+
await writeBytecode(result.code, join(outDir, name + ".srt.bin"))
|
|
189
|
+
clearIsolates(isolatesDir, ".bin")
|
|
190
|
+
for (let isolate of result.isolates) {
|
|
191
|
+
await writeIsolateBytecode(isolatesDir, isolate)
|
|
192
|
+
}
|
|
193
|
+
process.exit()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let result = await bundleSolid(mode)
|
|
197
|
+
let jsOutfile = join(outDir, name + ".srt.js")
|
|
198
|
+
await Bun.write(jsOutfile, result.code)
|
|
199
|
+
clearIsolates(isolatesDir, ".js")
|
|
200
|
+
writeIsolates(isolatesDir, result.isolates)
|
|
201
|
+
console.log(`>> wrote ${result.code.length} bytes to ${jsOutfile}`)
|
|
202
|
+
process.exit()
|
|
203
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# srt check
|
|
2
|
+
|
|
3
|
+
{{ usage check }}
|
|
4
|
+
|
|
5
|
+
The gate worth wiring into CI: it builds and typechecks the app without
|
|
6
|
+
producing build output. With a folder it covers every entry under it
|
|
7
|
+
(`src/index.tsx`, `examples/*`, and in a monorepo every example app,
|
|
8
|
+
package example and package demo), so one call at the repo root answers
|
|
9
|
+
"did I break any example" before pushing. The dev server runs the same check once at
|
|
10
|
+
startup, without gating on it.
|