create-mirinjs 0.0.1-alpha.0

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 ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "create-mirinjs",
3
+ "version": "0.0.1-alpha.0",
4
+ "description": "Scaffold a new mirin desktop app: bun create mirinjs",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/Netko-Labs/mirin#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Netko-Labs/mirin.git",
11
+ "directory": "packages/create-mirinjs"
12
+ },
13
+ "bugs": "https://github.com/Netko-Labs/mirin/issues",
14
+ "keywords": [
15
+ "mirinjs",
16
+ "create",
17
+ "scaffold",
18
+ "desktop",
19
+ "bun"
20
+ ],
21
+ "bin": {
22
+ "create-mirinjs": "./src/index.ts"
23
+ },
24
+ "exports": {
25
+ ".": "./src/scaffold.ts"
26
+ },
27
+ "files": [
28
+ "src",
29
+ "template"
30
+ ],
31
+ "engines": {
32
+ "bun": ">=1.2.0"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ }
37
+ }
package/src/index.ts ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * `bun create mirinjs [dir]` — scaffold a new mirin app.
4
+ */
5
+
6
+ import { resolve } from "node:path";
7
+ import { scaffold } from "./scaffold.ts";
8
+
9
+ const target = Bun.argv[2] ?? "my-mirin-app";
10
+ const targetDir = resolve(process.cwd(), target);
11
+
12
+ try {
13
+ const name = scaffold(targetDir);
14
+ console.log(`\n✓ Created ${name} in ${target}\n`);
15
+ console.log("Next steps:");
16
+ console.log(` cd ${target}`);
17
+ console.log(" bun install");
18
+ console.log(" bun run dev # launches a native window with HMR + typed RPC");
19
+ console.log(" bun run build # packages a standalone .app\n");
20
+ console.log("Requires macOS arm64, Bun, and the Xcode command-line tools.");
21
+ } catch (err) {
22
+ console.error(`create-mirinjs: ${err instanceof Error ? err.message : err}`);
23
+ process.exit(1);
24
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Copy the starter template into a target directory, substituting the app name,
3
+ * id, and the mirin version. Shared by `bun create mirinjs` and `mirin init`.
4
+ */
5
+
6
+ import {
7
+ cpSync,
8
+ existsSync,
9
+ readFileSync,
10
+ readdirSync,
11
+ renameSync,
12
+ statSync,
13
+ writeFileSync,
14
+ } from "node:fs";
15
+ import { join } from "node:path";
16
+
17
+ const TEMPLATE_DIR = join(import.meta.dir, "..", "template");
18
+
19
+ export interface ScaffoldOptions {
20
+ /** App / package name (kebab-case). Defaults to the directory name. */
21
+ name?: string;
22
+ }
23
+
24
+ /** Scaffold a new mirin app into `targetDir`. Returns the resolved app name. */
25
+ export function scaffold(targetDir: string, options: ScaffoldOptions = {}): string {
26
+ if (existsSync(targetDir) && readdirSync(targetDir).length > 0) {
27
+ throw new Error(`target directory "${targetDir}" already exists and is not empty.`);
28
+ }
29
+ const appName = (options.name ?? basename(targetDir)).trim() || "mirin-app";
30
+ const appId = `dev.local.${appName.replace(/[^a-z0-9]+/gi, "").toLowerCase() || "app"}`;
31
+ const version = mirinVersion();
32
+
33
+ cpSync(TEMPLATE_DIR, targetDir, { recursive: true });
34
+
35
+ // npm strips a literal .gitignore from published packages; ship it as
36
+ // `_gitignore` and restore the name on scaffold.
37
+ const ignore = join(targetDir, "_gitignore");
38
+ if (existsSync(ignore)) renameSync(ignore, join(targetDir, ".gitignore"));
39
+
40
+ const replacements: Record<string, string> = {
41
+ __APP_NAME__: appName,
42
+ __APP_ID__: appId,
43
+ __MIRIN_VERSION__: version,
44
+ };
45
+ for (const file of walk(targetDir)) applyReplacements(file, replacements);
46
+
47
+ return appName;
48
+ }
49
+
50
+ function applyReplacements(file: string, replacements: Record<string, string>): void {
51
+ let text: string;
52
+ try {
53
+ text = readFileSync(file, "utf8");
54
+ } catch {
55
+ return; // skip binary/unreadable files
56
+ }
57
+ let changed = text;
58
+ for (const [from, to] of Object.entries(replacements)) {
59
+ changed = changed.split(from).join(to);
60
+ }
61
+ if (changed !== text) writeFileSync(file, changed);
62
+ }
63
+
64
+ function walk(dir: string): string[] {
65
+ const out: string[] = [];
66
+ for (const entry of readdirSync(dir)) {
67
+ const full = join(dir, entry);
68
+ if (statSync(full).isDirectory()) out.push(...walk(full));
69
+ else out.push(full);
70
+ }
71
+ return out;
72
+ }
73
+
74
+ function basename(path: string): string {
75
+ return path.replace(/\/+$/, "").split("/").pop() ?? "";
76
+ }
77
+
78
+ /** The version of this create-mirin package, pinned into the scaffold. */
79
+ function mirinVersion(): string {
80
+ const pkg = JSON.parse(readFileSync(join(import.meta.dir, "..", "package.json"), "utf8"));
81
+ return `^${pkg.version}`;
82
+ }
@@ -0,0 +1,16 @@
1
+ # __APP_NAME__
2
+
3
+ A desktop app built with [mirin](https://github.com/Netko-Labs/mirin) — Bun +
4
+ TypeScript + Chromium.
5
+
6
+ ```bash
7
+ bun install
8
+ bun run dev # native window with Vite HMR + typed RPC
9
+ bun run build # standalone .app in ./build
10
+ ```
11
+
12
+ - `mirin.config.ts` — the app manifest (windows, ids).
13
+ - `main/` — the Bun main process (RPC handlers, app lifecycle).
14
+ - `ui/` — the React UI, served via the `app://` scheme in builds.
15
+
16
+ Requires macOS arm64, Bun, and the Xcode command-line tools.
@@ -0,0 +1,5 @@
1
+ node_modules/
2
+ dist/
3
+ build/
4
+ .mirin/
5
+ .DS_Store
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>__APP_NAME__</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/ui/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,16 @@
1
+ import { app } from "mirinjs";
2
+ import { router } from "./rpc.ts";
3
+
4
+ const mirin = app.serve(router);
5
+
6
+ app.on("ready", () => {
7
+ // Push a typed tick to every webview once a second.
8
+ let count = 0;
9
+ setInterval(() => {
10
+ mirin.rpc.tick.broadcast({ count: ++count });
11
+ }, 1000);
12
+ });
13
+
14
+ app.on("window-all-closed", () => {
15
+ app.quit();
16
+ });
@@ -0,0 +1,14 @@
1
+ import { rpc } from "mirinjs/rpc";
2
+
3
+ /** The app's RPC surface — imported by the main process (handlers) and, as a
4
+ * type only, by the UI (`mirin/client`). */
5
+ export const router = rpc.router({
6
+ greet: rpc.query(async (name: string, ctx) => {
7
+ return `Hello, ${name}! (from webview #${ctx.webview}, Bun ${Bun.version})`;
8
+ }),
9
+
10
+ // main -> UI push
11
+ tick: rpc.event<{ count: number }>(),
12
+ });
13
+
14
+ export type Router = typeof router;
@@ -0,0 +1,17 @@
1
+ import { defineConfig } from "mirinjs/config";
2
+
3
+ export default defineConfig({
4
+ id: "__APP_ID__",
5
+ name: "__APP_NAME__",
6
+ main: "main/main.ts",
7
+
8
+ windows: {
9
+ main: {
10
+ title: "__APP_NAME__",
11
+ width: 960,
12
+ height: 700,
13
+ url: "app://ui/index.html",
14
+ show: "ready",
15
+ },
16
+ },
17
+ });
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "__APP_NAME__",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "mirin dev",
7
+ "build": "mirin build"
8
+ },
9
+ "dependencies": {
10
+ "mirinjs": "__MIRIN_VERSION__",
11
+ "react": "^19.2.0",
12
+ "react-dom": "^19.2.0"
13
+ },
14
+ "devDependencies": {
15
+ "@mirinjs/cli": "__MIRIN_VERSION__",
16
+ "vite": "npm:rolldown-vite@^7.3.1",
17
+ "@vitejs/plugin-react": "^6.0.2",
18
+ "@types/react": "^19.2.0",
19
+ "@types/react-dom": "^19.2.0",
20
+ "@types/bun": "^1.2.0",
21
+ "typescript": "^5.8.0"
22
+ }
23
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "moduleDetection": "force",
7
+ "allowImportingTsExtensions": true,
8
+ "verbatimModuleSyntax": true,
9
+ "strict": true,
10
+ "noUncheckedIndexedAccess": true,
11
+ "noEmit": true,
12
+ "skipLibCheck": true,
13
+ "jsx": "react-jsx",
14
+ "types": ["bun"],
15
+ "lib": ["ESNext", "DOM", "DOM.Iterable"]
16
+ },
17
+ "include": ["mirin.config.ts", "main", "ui", "vite.config.ts"]
18
+ }
@@ -0,0 +1,41 @@
1
+ import { useEffect, useState } from "react";
2
+ import { api } from "./api.ts";
3
+
4
+ export function App() {
5
+ const [greeting, setGreeting] = useState("…");
6
+ const [name, setName] = useState("world");
7
+ const [ticks, setTicks] = useState(0);
8
+
9
+ useEffect(() => {
10
+ api.greet(name).then(setGreeting);
11
+ }, [name]);
12
+
13
+ useEffect(() => api.tick.on(({ count }) => setTicks(count)), []);
14
+
15
+ return (
16
+ <main
17
+ style={{
18
+ fontFamily: "system-ui, sans-serif",
19
+ padding: "3rem",
20
+ maxWidth: 560,
21
+ margin: "0 auto",
22
+ color: "#1a1a2e",
23
+ }}
24
+ >
25
+ <h1>__APP_NAME__</h1>
26
+ <p style={{ color: "#6b7280" }}>
27
+ Bun main process · CEF webview · typed RPC over a localhost socket.
28
+ </p>
29
+
30
+ <p style={{ fontSize: 18 }}>{greeting}</p>
31
+ <input
32
+ value={name}
33
+ onChange={(e) => setName(e.target.value)}
34
+ placeholder="your name"
35
+ style={{ padding: "8px 10px", borderRadius: 8, border: "1px solid #d1d5db" }}
36
+ />
37
+
38
+ <p style={{ fontSize: 28, marginTop: 24 }}>tick #{ticks}</p>
39
+ </main>
40
+ );
41
+ }
@@ -0,0 +1,5 @@
1
+ import { client } from "mirinjs/client";
2
+ import type { Router } from "../main/rpc.ts";
3
+
4
+ /** Typed RPC client — full inference from the main-process router, no codegen. */
5
+ export const api = client<Router>();
@@ -0,0 +1,9 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { App } from "./App.tsx";
4
+
5
+ createRoot(document.getElementById("root")!).render(
6
+ <StrictMode>
7
+ <App />
8
+ </StrictMode>,
9
+ );
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ export default defineConfig({
5
+ root: ".",
6
+ base: "./",
7
+ server: { port: 5173, strictPort: true },
8
+ plugins: [react()],
9
+ });