specli 0.0.1 → 0.0.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.
- package/README.md +76 -86
- package/cli.ts +4 -10
- package/package.json +8 -2
- package/src/cli/compile.ts +5 -28
- package/src/cli/derive-name.ts +2 -2
- package/src/cli/exec.ts +1 -1
- package/src/cli/main.ts +12 -27
- package/src/cli/runtime/auth/resolve.ts +10 -2
- package/src/cli/runtime/body-flags.ts +176 -0
- package/src/cli/runtime/execute.ts +41 -91
- package/src/cli/runtime/generated.ts +28 -93
- package/src/cli/runtime/profile/secrets.ts +1 -1
- package/src/cli/runtime/profile/store.ts +1 -1
- package/src/cli/runtime/request.ts +42 -152
- package/src/cli/stable-json.ts +2 -2
- package/src/compiled.ts +13 -15
- package/src/macros/env.ts +0 -4
- package/CLAUDE.md +0 -111
- package/PLAN.md +0 -274
- package/biome.jsonc +0 -1
- package/bun.lock +0 -98
- package/fixtures/openapi-array-items.json +0 -22
- package/fixtures/openapi-auth.json +0 -34
- package/fixtures/openapi-body.json +0 -41
- package/fixtures/openapi-collision.json +0 -21
- package/fixtures/openapi-oauth.json +0 -54
- package/fixtures/openapi-servers.json +0 -35
- package/fixtures/openapi.json +0 -87
- package/scripts/smoke-specs.ts +0 -64
- package/src/cli/auth-requirements.test.ts +0 -27
- package/src/cli/auth-schemes.test.ts +0 -66
- package/src/cli/capabilities.test.ts +0 -94
- package/src/cli/command-id.test.ts +0 -32
- package/src/cli/command-model.test.ts +0 -44
- package/src/cli/naming.test.ts +0 -86
- package/src/cli/operations.test.ts +0 -57
- package/src/cli/params.test.ts +0 -70
- package/src/cli/positional.test.ts +0 -65
- package/src/cli/request-body.test.ts +0 -35
- package/src/cli/runtime/request.test.ts +0 -153
- package/src/cli/server.test.ts +0 -35
- package/tsconfig.json +0 -29
|
@@ -2,8 +2,6 @@ import type { AuthScheme } from "../auth-schemes.ts";
|
|
|
2
2
|
import type { CommandAction } from "../command-model.ts";
|
|
3
3
|
|
|
4
4
|
import { resolveAuthScheme } from "./auth/resolve.ts";
|
|
5
|
-
import { loadBody, parseBodyAsJsonOrYaml } from "./body.ts";
|
|
6
|
-
import { parseHeaderInput } from "./headers.ts";
|
|
7
5
|
import { getToken } from "./profile/secrets.ts";
|
|
8
6
|
import { getProfile, readProfiles } from "./profile/store.ts";
|
|
9
7
|
import { resolveServerUrl } from "./server-url.ts";
|
|
@@ -19,30 +17,7 @@ export type RuntimeGlobals = {
|
|
|
19
17
|
server?: string;
|
|
20
18
|
serverVar?: string[];
|
|
21
19
|
|
|
22
|
-
// Common runtime flags (may conflict with operation flags).
|
|
23
|
-
header?: string[];
|
|
24
|
-
accept?: string;
|
|
25
|
-
contentType?: string;
|
|
26
|
-
data?: string;
|
|
27
|
-
file?: string;
|
|
28
|
-
timeout?: string;
|
|
29
|
-
dryRun?: boolean;
|
|
30
20
|
curl?: boolean;
|
|
31
|
-
status?: boolean;
|
|
32
|
-
headers?: boolean;
|
|
33
|
-
|
|
34
|
-
// Namespaced variants for the runtime flags above.
|
|
35
|
-
ocHeader?: string[];
|
|
36
|
-
ocAccept?: string;
|
|
37
|
-
ocContentType?: string;
|
|
38
|
-
ocData?: string;
|
|
39
|
-
ocFile?: string;
|
|
40
|
-
ocTimeout?: string;
|
|
41
|
-
ocDryRun?: boolean;
|
|
42
|
-
ocCurl?: boolean;
|
|
43
|
-
ocStatus?: boolean;
|
|
44
|
-
ocHeaders?: boolean;
|
|
45
|
-
|
|
46
21
|
json?: boolean;
|
|
47
22
|
|
|
48
23
|
auth?: string;
|
|
@@ -159,6 +134,12 @@ function applyAuth(
|
|
|
159
134
|
return { headers, url };
|
|
160
135
|
}
|
|
161
136
|
|
|
137
|
+
export type EmbeddedDefaults = {
|
|
138
|
+
server?: string;
|
|
139
|
+
serverVars?: string[];
|
|
140
|
+
auth?: string;
|
|
141
|
+
};
|
|
142
|
+
|
|
162
143
|
export type BuildRequestInput = {
|
|
163
144
|
specId: string;
|
|
164
145
|
action: CommandAction;
|
|
@@ -167,6 +148,8 @@ export type BuildRequestInput = {
|
|
|
167
148
|
globals: RuntimeGlobals;
|
|
168
149
|
servers: import("../server.ts").ServerInfo[];
|
|
169
150
|
authSchemes: AuthScheme[];
|
|
151
|
+
embeddedDefaults?: EmbeddedDefaults;
|
|
152
|
+
bodyFlagDefs?: import("./body-flags.ts").BodyFlagDef[];
|
|
170
153
|
};
|
|
171
154
|
|
|
172
155
|
export async function buildRequest(
|
|
@@ -174,10 +157,16 @@ export async function buildRequest(
|
|
|
174
157
|
): Promise<{ request: Request; curl: string }> {
|
|
175
158
|
const profilesFile = await readProfiles();
|
|
176
159
|
const profile = getProfile(profilesFile, input.globals.profile);
|
|
160
|
+
const embedded = input.embeddedDefaults;
|
|
161
|
+
|
|
162
|
+
// Merge server vars: CLI flags override embedded defaults
|
|
163
|
+
const embeddedServerVars = parseKeyValuePairs(embedded?.serverVars);
|
|
164
|
+
const cliServerVars = parseKeyValuePairs(input.globals.serverVar);
|
|
165
|
+
const serverVars = { ...embeddedServerVars, ...cliServerVars };
|
|
177
166
|
|
|
178
|
-
|
|
167
|
+
// Priority: CLI flag > profile > embedded default
|
|
179
168
|
const serverUrl = resolveServerUrl({
|
|
180
|
-
serverOverride: input.globals.server ?? profile?.server,
|
|
169
|
+
serverOverride: input.globals.server ?? profile?.server ?? embedded?.server,
|
|
181
170
|
servers: input.servers,
|
|
182
171
|
serverVars,
|
|
183
172
|
});
|
|
@@ -205,8 +194,6 @@ export async function buildRequest(
|
|
|
205
194
|
);
|
|
206
195
|
|
|
207
196
|
const headers = new Headers();
|
|
208
|
-
const accept = input.globals.accept ?? input.globals.ocAccept;
|
|
209
|
-
if (accept) headers.set("Accept", accept);
|
|
210
197
|
|
|
211
198
|
// Collect declared params for validation.
|
|
212
199
|
const queryValues: Record<string, unknown> = {};
|
|
@@ -271,102 +258,45 @@ export async function buildRequest(
|
|
|
271
258
|
headers.set("Cookie", existing ? `${existing}; ${part}` : part);
|
|
272
259
|
}
|
|
273
260
|
|
|
274
|
-
const extraHeaders = [
|
|
275
|
-
...(input.globals.header ?? []),
|
|
276
|
-
...(input.globals.ocHeader ?? []),
|
|
277
|
-
].map(parseHeaderInput);
|
|
278
|
-
for (const { name, value } of extraHeaders) {
|
|
279
|
-
headers.set(name, value);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
261
|
let body: string | undefined;
|
|
283
262
|
if (input.action.requestBody) {
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
: {};
|
|
294
|
-
|
|
295
|
-
const bodyFlags: Record<string, unknown> = Object.fromEntries(
|
|
296
|
-
Object.entries(rawBodyFlags).filter(
|
|
297
|
-
([, v]) => typeof v !== "undefined" && v !== false,
|
|
298
|
-
),
|
|
299
|
-
);
|
|
300
|
-
|
|
301
|
-
const hasExpandedBody = Object.keys(bodyFlags).length > 0;
|
|
302
|
-
|
|
303
|
-
if (hasData && hasFile) throw new Error("Use only one of --data or --file");
|
|
304
|
-
if (hasExpandedBody && (hasData || hasFile)) {
|
|
305
|
-
throw new Error("Use either --data/--file or --body-* flags (not both)");
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const contentType =
|
|
309
|
-
input.globals.contentType ??
|
|
310
|
-
input.globals.ocContentType ??
|
|
311
|
-
input.action.requestBody.preferredContentType;
|
|
263
|
+
// Check if any body flags were provided using the flag definitions
|
|
264
|
+
const bodyFlagDefs = input.bodyFlagDefs ?? [];
|
|
265
|
+
const hasBodyFlags = bodyFlagDefs.some((def) => {
|
|
266
|
+
// Commander keeps dots in option names: --address.street -> "address.street"
|
|
267
|
+
const dotKey = def.path.join(".");
|
|
268
|
+
return input.flagValues[dotKey] !== undefined;
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const contentType = input.action.requestBody.preferredContentType;
|
|
312
272
|
if (contentType) headers.set("Content-Type", contentType);
|
|
313
273
|
|
|
314
274
|
const schema = input.action.requestBodySchema;
|
|
315
275
|
|
|
316
|
-
if (!
|
|
276
|
+
if (!hasBodyFlags) {
|
|
317
277
|
if (input.action.requestBody.required) {
|
|
318
|
-
throw new Error(
|
|
319
|
-
"Missing request body. Provide --data, --file, or --body-* flags.",
|
|
320
|
-
);
|
|
278
|
+
throw new Error("Missing required request body fields.");
|
|
321
279
|
}
|
|
322
|
-
} else
|
|
280
|
+
} else {
|
|
323
281
|
if (!contentType?.includes("json")) {
|
|
324
282
|
throw new Error(
|
|
325
|
-
"
|
|
283
|
+
"Body field flags are only supported for JSON request bodies.",
|
|
326
284
|
);
|
|
327
285
|
}
|
|
328
286
|
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
}
|
|
287
|
+
// Check for missing required fields
|
|
288
|
+
const { findMissingRequired, parseDotNotationFlags } = await import(
|
|
289
|
+
"./body-flags.ts"
|
|
290
|
+
);
|
|
291
|
+
const missing = findMissingRequired(input.flagValues, bodyFlagDefs);
|
|
292
|
+
if (missing.length > 0) {
|
|
293
|
+
throw new Error(
|
|
294
|
+
`Missing required body field '${missing[0]}'. Provide --${missing[0]}.`,
|
|
295
|
+
);
|
|
340
296
|
}
|
|
341
297
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (!k.startsWith("body")) continue;
|
|
345
|
-
const name = k.slice("body".length);
|
|
346
|
-
const propName = name.length
|
|
347
|
-
? name[0]?.toLowerCase() + name.slice(1)
|
|
348
|
-
: "";
|
|
349
|
-
if (!propName) continue;
|
|
350
|
-
|
|
351
|
-
const propSchema =
|
|
352
|
-
schema && schema.type === "object" && schema.properties
|
|
353
|
-
? (schema.properties as Record<string, unknown>)[propName]
|
|
354
|
-
: undefined;
|
|
355
|
-
const t =
|
|
356
|
-
propSchema && typeof propSchema === "object"
|
|
357
|
-
? (propSchema as { type?: unknown }).type
|
|
358
|
-
: "string";
|
|
359
|
-
|
|
360
|
-
if (t === "boolean") {
|
|
361
|
-
built[propName] = true;
|
|
362
|
-
} else if (t === "integer") {
|
|
363
|
-
built[propName] = Number.parseInt(String(v), 10);
|
|
364
|
-
} else if (t === "number") {
|
|
365
|
-
built[propName] = Number(String(v));
|
|
366
|
-
} else {
|
|
367
|
-
built[propName] = String(v);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
298
|
+
// Build nested object from dot-notation flags
|
|
299
|
+
const built = parseDotNotationFlags(input.flagValues, bodyFlagDefs);
|
|
370
300
|
|
|
371
301
|
if (schema) {
|
|
372
302
|
const validate = ajv.compile(schema);
|
|
@@ -376,57 +306,17 @@ export async function buildRequest(
|
|
|
376
306
|
}
|
|
377
307
|
|
|
378
308
|
body = JSON.stringify(built);
|
|
379
|
-
} else if (hasData) {
|
|
380
|
-
if (contentType?.includes("json")) {
|
|
381
|
-
const parsed = parseBodyAsJsonOrYaml(data as string);
|
|
382
|
-
|
|
383
|
-
if (schema) {
|
|
384
|
-
const validate = ajv.compile(schema);
|
|
385
|
-
if (!validate(parsed)) {
|
|
386
|
-
throw new Error(formatAjvErrors(validate.errors));
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
body = JSON.stringify(parsed);
|
|
391
|
-
} else {
|
|
392
|
-
body = data as string;
|
|
393
|
-
}
|
|
394
|
-
} else if (hasFile) {
|
|
395
|
-
const loaded = await loadBody({
|
|
396
|
-
kind: "file",
|
|
397
|
-
path: file as string,
|
|
398
|
-
});
|
|
399
|
-
if (contentType?.includes("json")) {
|
|
400
|
-
const parsed = parseBodyAsJsonOrYaml(loaded?.raw ?? "");
|
|
401
|
-
|
|
402
|
-
if (schema) {
|
|
403
|
-
const validate = ajv.compile(schema);
|
|
404
|
-
if (!validate(parsed)) {
|
|
405
|
-
throw new Error(formatAjvErrors(validate.errors));
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
body = JSON.stringify(parsed);
|
|
410
|
-
} else {
|
|
411
|
-
body = loaded?.raw;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
} else {
|
|
415
|
-
if (
|
|
416
|
-
typeof (input.globals.data ?? input.globals.ocData) === "string" ||
|
|
417
|
-
typeof (input.globals.file ?? input.globals.ocFile) === "string"
|
|
418
|
-
) {
|
|
419
|
-
throw new Error("This operation does not accept a request body");
|
|
420
309
|
}
|
|
421
310
|
}
|
|
422
311
|
|
|
423
|
-
//
|
|
312
|
+
// Auth resolution priority: CLI flag > profile > embedded default
|
|
424
313
|
const resolvedAuthScheme = resolveAuthScheme(
|
|
425
314
|
input.authSchemes,
|
|
426
315
|
input.action.auth,
|
|
427
316
|
{
|
|
428
317
|
flagAuthScheme: input.globals.auth,
|
|
429
318
|
profileAuthScheme: profile?.authScheme,
|
|
319
|
+
embeddedAuthScheme: embedded?.auth,
|
|
430
320
|
},
|
|
431
321
|
);
|
|
432
322
|
|
package/src/cli/stable-json.ts
CHANGED
|
@@ -10,7 +10,7 @@ function sort(value: unknown, visiting: WeakSet<object>): unknown {
|
|
|
10
10
|
if (value === null) return null;
|
|
11
11
|
|
|
12
12
|
if (Array.isArray(value)) {
|
|
13
|
-
if (visiting.has(value)) return {
|
|
13
|
+
if (visiting.has(value)) return { __specli_circular: true };
|
|
14
14
|
visiting.add(value);
|
|
15
15
|
const out = value.map((v) => sort(v, visiting));
|
|
16
16
|
visiting.delete(value);
|
|
@@ -18,7 +18,7 @@ function sort(value: unknown, visiting: WeakSet<object>): unknown {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
if (typeof value === "object") {
|
|
21
|
-
if (visiting.has(value)) return {
|
|
21
|
+
if (visiting.has(value)) return { __specli_circular: true };
|
|
22
22
|
visiting.add(value);
|
|
23
23
|
|
|
24
24
|
const obj = value as Record<string, unknown>;
|
package/src/compiled.ts
CHANGED
|
@@ -5,19 +5,17 @@ import { env, envRequired } from "./macros/env.ts" with { type: "macro" };
|
|
|
5
5
|
import { loadSpec } from "./macros/spec.ts" with { type: "macro" };
|
|
6
6
|
|
|
7
7
|
// This entrypoint is intended to be compiled.
|
|
8
|
-
//
|
|
9
|
-
const embeddedSpecText = await loadSpec(envRequired("
|
|
8
|
+
// All values are embedded via Bun macros at bundle-time.
|
|
9
|
+
const embeddedSpecText = await loadSpec(envRequired("SPECLI_SPEC"));
|
|
10
|
+
const cliName = env("SPECLI_NAME");
|
|
11
|
+
const server = env("SPECLI_SERVER");
|
|
12
|
+
const serverVars = env("SPECLI_SERVER_VARS");
|
|
13
|
+
const auth = env("SPECLI_AUTH");
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
process.argv[1] ?? cliName ?? "opencli",
|
|
19
|
-
...(process.execArgv ?? []),
|
|
20
|
-
...process.argv.slice(2),
|
|
21
|
-
];
|
|
22
|
-
|
|
23
|
-
await main(argv, { embeddedSpecText, cliName });
|
|
15
|
+
await main(process.argv, {
|
|
16
|
+
embeddedSpecText,
|
|
17
|
+
cliName,
|
|
18
|
+
server,
|
|
19
|
+
serverVars: serverVars ? serverVars.split(",") : undefined,
|
|
20
|
+
auth,
|
|
21
|
+
});
|
package/src/macros/env.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bun macro: reads an environment variable at bundle-time.
|
|
3
3
|
* Returns undefined if the env var is not set.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* import { env } from "./macros/env.ts" with { type: "macro" };
|
|
7
|
-
* const value = env("MY_VAR");
|
|
8
4
|
*/
|
|
9
5
|
export function env(name: string): string | undefined {
|
|
10
6
|
if (!name) throw new Error("env macro: missing variable name");
|
package/CLAUDE.md
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Use Bun instead of Node.js, npm, pnpm, or vite.
|
|
3
|
-
globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
|
|
4
|
-
alwaysApply: false
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
Default to using Bun instead of Node.js.
|
|
8
|
-
|
|
9
|
-
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
|
10
|
-
- Use `bun test` instead of `jest` or `vitest`
|
|
11
|
-
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
|
12
|
-
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
|
13
|
-
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
|
14
|
-
- Use `bunx <package> <command>` instead of `npx <package> <command>`
|
|
15
|
-
- Bun automatically loads .env, so don't use dotenv.
|
|
16
|
-
|
|
17
|
-
## APIs
|
|
18
|
-
|
|
19
|
-
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
|
20
|
-
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
|
21
|
-
- `Bun.redis` for Redis. Don't use `ioredis`.
|
|
22
|
-
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
|
23
|
-
- `WebSocket` is built-in. Don't use `ws`.
|
|
24
|
-
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
|
25
|
-
- Bun.$`ls` instead of execa.
|
|
26
|
-
|
|
27
|
-
## Testing
|
|
28
|
-
|
|
29
|
-
Use `bun test` to run tests.
|
|
30
|
-
|
|
31
|
-
```ts#index.test.ts
|
|
32
|
-
import { test, expect } from "bun:test";
|
|
33
|
-
|
|
34
|
-
test("hello world", () => {
|
|
35
|
-
expect(1).toBe(1);
|
|
36
|
-
});
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## Frontend
|
|
40
|
-
|
|
41
|
-
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
|
42
|
-
|
|
43
|
-
Server:
|
|
44
|
-
|
|
45
|
-
```ts#index.ts
|
|
46
|
-
import index from "./index.html"
|
|
47
|
-
|
|
48
|
-
Bun.serve({
|
|
49
|
-
routes: {
|
|
50
|
-
"/": index,
|
|
51
|
-
"/api/users/:id": {
|
|
52
|
-
GET: (req) => {
|
|
53
|
-
return new Response(JSON.stringify({ id: req.params.id }));
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
// optional websocket support
|
|
58
|
-
websocket: {
|
|
59
|
-
open: (ws) => {
|
|
60
|
-
ws.send("Hello, world!");
|
|
61
|
-
},
|
|
62
|
-
message: (ws, message) => {
|
|
63
|
-
ws.send(message);
|
|
64
|
-
},
|
|
65
|
-
close: (ws) => {
|
|
66
|
-
// handle close
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
development: {
|
|
70
|
-
hmr: true,
|
|
71
|
-
console: true,
|
|
72
|
-
}
|
|
73
|
-
})
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
|
77
|
-
|
|
78
|
-
```html#index.html
|
|
79
|
-
<html>
|
|
80
|
-
<body>
|
|
81
|
-
<h1>Hello, world!</h1>
|
|
82
|
-
<script type="module" src="./frontend.tsx"></script>
|
|
83
|
-
</body>
|
|
84
|
-
</html>
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
With the following `frontend.tsx`:
|
|
88
|
-
|
|
89
|
-
```tsx#frontend.tsx
|
|
90
|
-
import React from "react";
|
|
91
|
-
import { createRoot } from "react-dom/client";
|
|
92
|
-
|
|
93
|
-
// import .css files directly and it works
|
|
94
|
-
import './index.css';
|
|
95
|
-
|
|
96
|
-
const root = createRoot(document.body);
|
|
97
|
-
|
|
98
|
-
export default function Frontend() {
|
|
99
|
-
return <h1>Hello, world!</h1>;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
root.render(<Frontend />);
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
Then, run index.ts
|
|
106
|
-
|
|
107
|
-
```sh
|
|
108
|
-
bun --hot ./index.ts
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
|