murasaki 0.0.0 → 0.0.2

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,24 @@
1
+ #!/usr/bin/env node
2
+ // Murasaki CLI entry point.
3
+ // Usage: murasaki dev
4
+
5
+ import 'tsx/esm' // Register the TS+JSX ESM loader
6
+ import { fileURLToPath } from 'node:url'
7
+ import { dirname, resolve } from 'node:path'
8
+ import { pathToFileURL } from 'node:url'
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url))
11
+ const cmd = process.argv[2]
12
+
13
+ if (cmd === 'dev') {
14
+ process.env.MURASAKI_DEV = '1'
15
+ const devPath = resolve(__dirname, '..', 'src', 'dev.tsx')
16
+ await import(pathToFileURL(devPath).href)
17
+ } else {
18
+ process.stdout.write(`
19
+ Usage:
20
+ murasaki dev Start the development server (HMR)
21
+
22
+ `)
23
+ process.exit(cmd ? 1 : 0)
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "murasaki",
3
- "version": "0.0.0",
3
+ "version": "0.0.2",
4
4
  "description": "The desktop framework for Next.js developers. Node-powered. WebView-thin. No Rust. No Chromium.",
5
5
  "keywords": [
6
6
  "desktop",
@@ -23,8 +23,28 @@
23
23
  "license": "MIT",
24
24
  "author": "ichi",
25
25
  "type": "module",
26
+ "bin": {
27
+ "murasaki": "./bin/murasaki.js"
28
+ },
26
29
  "files": [
30
+ "bin",
31
+ "src",
27
32
  "README.md",
28
33
  "LICENSE"
29
- ]
34
+ ],
35
+ "scripts": {
36
+ "example:hello": "MURASAKI_DEV=1 tsx examples/hello/index.tsx",
37
+ "example:app-router": "cd examples/app-router && pnpm dev"
38
+ },
39
+ "dependencies": {
40
+ "@webviewjs/webview": "^0.3.2",
41
+ "react": "^19.2.7",
42
+ "react-dom": "^19.2.7",
43
+ "tsx": "^4.22.4"
44
+ },
45
+ "devDependencies": {
46
+ "@types/react": "^19.2.17",
47
+ "@types/react-dom": "^19.2.3",
48
+ "typescript": "^6.0.3"
49
+ }
30
50
  }
package/src/dev.tsx ADDED
@@ -0,0 +1,200 @@
1
+ // src/dev.tsx
2
+ // Murasaki dev runner — Next.js-like file-based routing without Next.js.
3
+ //
4
+ // Reads the consumer's app/ directory:
5
+ // app/layout.tsx (optional)
6
+ // app/page.tsx (required)
7
+ //
8
+ // Renders <Layout><Page /></Layout> with React, ships HTML to the WebView,
9
+ // and reloads in place on file change.
10
+
11
+ import { Application, ControlFlow } from '@webviewjs/webview'
12
+ import { existsSync, readFileSync, watch } from 'node:fs'
13
+ import { fileURLToPath, pathToFileURL } from 'node:url'
14
+ import { dirname, join } from 'node:path'
15
+ import { createElement, type ComponentType, type ReactNode } from 'react'
16
+ import { renderToStaticMarkup } from 'react-dom/server'
17
+
18
+ // ── ANSI ──────────────────────────────────────────────────────────────
19
+ const BRIGHT = '\x1b[38;2;168;85;247m'
20
+ const DIM = '\x1b[38;2;136;136;153m'
21
+ const GREEN = '\x1b[38;2;76;175;80m'
22
+ const RED = '\x1b[38;2;239;68;68m'
23
+ const BOLD = '\x1b[1m'
24
+ const RESET = '\x1b[0m'
25
+ const noColor = process.env.NO_COLOR || !process.stdout.isTTY
26
+ const c = (code: string) => (noColor ? '' : code)
27
+
28
+ // ── Project paths ─────────────────────────────────────────────────────
29
+ const projectRoot = process.cwd()
30
+ const SRC_DIR = join(projectRoot, 'src')
31
+ const APP_PATH = join(SRC_DIR, 'app.tsx')
32
+ const LAYOUT_PATH = join(SRC_DIR, 'layout.tsx')
33
+
34
+ // ── Resolve murasaki version (for the banner) ─────────────────────────
35
+ const __dirname = dirname(fileURLToPath(import.meta.url))
36
+ let VERSION = '0.0.0'
37
+ try {
38
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'))
39
+ VERSION = pkg.version || '0.0.0'
40
+ } catch {}
41
+
42
+ const WEBVIEW_ENGINE = (() => {
43
+ switch (process.platform) {
44
+ case 'darwin': return 'WKWebView (macOS)'
45
+ case 'win32': return 'WebView2 (Windows)'
46
+ case 'linux': return 'WebKitGTK (Linux)'
47
+ default: return `OS native (${process.platform})`
48
+ }
49
+ })()
50
+
51
+ const WIN_TITLE = 'Murasaki App'
52
+ const WIN_SIZE = { width: 1280, height: 800 }
53
+ const START_AT = Date.now()
54
+ const isDev = process.env.MURASAKI_DEV === '1' || true // dev runner always = dev
55
+
56
+ // ── Banner ────────────────────────────────────────────────────────────
57
+ function printBanner() {
58
+ process.stdout.write('\n')
59
+ process.stdout.write(` ${c(BOLD)}${c(BRIGHT)}🦋 Murasaki${c(RESET)} ${c(DIM)}${VERSION}${c(RESET)}\n\n`)
60
+ process.stdout.write(` ${c(DIM)}-${c(RESET)} ${c(DIM)}Project ${c(RESET)}${projectRoot}\n`)
61
+ process.stdout.write(` ${c(DIM)}-${c(RESET)} ${c(DIM)}Window ${c(RESET)}${WIN_TITLE} ${c(DIM)}(${WIN_SIZE.width}×${WIN_SIZE.height})${c(RESET)}\n`)
62
+ process.stdout.write(` ${c(DIM)}-${c(RESET)} ${c(DIM)}Webview ${c(RESET)}${WEBVIEW_ENGINE}\n`)
63
+ process.stdout.write(` ${c(DIM)}-${c(RESET)} ${c(DIM)}Runtime ${c(RESET)}Node ${process.version}\n`)
64
+ process.stdout.write(` ${c(DIM)}-${c(RESET)} ${c(DIM)}Mode ${c(RESET)}development ${c(DIM)}(HMR active)${c(RESET)}\n\n`)
65
+ }
66
+ function printShortcuts() { process.stdout.write(` ${c(DIM)}Shortcuts ${c(RESET)}${c(BOLD)}o${c(RESET)} ${c(DIM)}open${c(RESET)} ${c(BOLD)}r${c(RESET)} ${c(DIM)}restart${c(RESET)} ${c(BOLD)}q${c(RESET)} ${c(DIM)}quit${c(RESET)}\n\n`) }
67
+ function printStarting() { process.stdout.write(` ${c(DIM)}○${c(RESET)} Starting...\n`) }
68
+ function printReady(ms: number) { process.stdout.write(` ${c(GREEN)}${c(BOLD)}✓${c(RESET)} ${c(BOLD)}Ready${c(RESET)} ${c(DIM)}in ${ms}ms${c(RESET)}\n`) }
69
+ function printOpened() { process.stdout.write(` ${c(GREEN)}${c(BOLD)}✓${c(RESET)} Window opened\n`) }
70
+ function printClosed() { process.stdout.write(` ${c(DIM)}○${c(RESET)} Window closed ${c(DIM)}— press ${c(RESET)}${c(BOLD)}o${c(RESET)}${c(DIM)} to re-open, ${c(RESET)}${c(BOLD)}q${c(RESET)}${c(DIM)} to quit${c(RESET)}\n`) }
71
+ function printReloaded(file: string) { process.stdout.write(` ${c(BRIGHT)}${c(BOLD)}↻${c(RESET)} Reloaded ${c(DIM)}${file}${c(RESET)}\n`) }
72
+ function printBye() { process.stdout.write(`\n ${c(DIM)}Bye 🦋${c(RESET)}\n\n`) }
73
+ function printHint(msg: string) { process.stdout.write(` ${c(DIM)}· ${msg}${c(RESET)}\n`) }
74
+ function printError(msg: string) { process.stdout.write(` ${c(RED)}${c(BOLD)}✗${c(RESET)} ${msg}\n`) }
75
+
76
+ // ── Routing: load app/page.tsx (+ optional app/layout.tsx) ────────────
77
+ type ReactComponent = ComponentType<{ children?: ReactNode }>
78
+
79
+ async function dynImport(path: string) {
80
+ // Cache-bust so file edits are picked up without restarting the process.
81
+ const url = pathToFileURL(path).href + `?v=${Date.now()}`
82
+ return import(url)
83
+ }
84
+
85
+ async function loadApp(): Promise<ReactComponent | null> {
86
+ if (!existsSync(APP_PATH)) return null
87
+ const mod = await dynImport(APP_PATH)
88
+ return mod.default as ReactComponent
89
+ }
90
+
91
+ async function loadLayout(): Promise<ReactComponent | null> {
92
+ if (!existsSync(LAYOUT_PATH)) return null
93
+ const mod = await dynImport(LAYOUT_PATH)
94
+ return mod.default as ReactComponent
95
+ }
96
+
97
+ async function renderApp(): Promise<string> {
98
+ const App = await loadApp()
99
+ if (!App) {
100
+ return '<!doctype html><html><body style="font-family:system-ui;padding:40px;"><h1 style="color:#A855F7">src/app.tsx not found</h1><p>Create one and the window will reload.</p></body></html>'
101
+ }
102
+ const Layout = await loadLayout()
103
+ const appEl = createElement(App)
104
+ const tree = Layout ? createElement(Layout, null, appEl) : appEl
105
+ return '<!doctype html>' + renderToStaticMarkup(tree)
106
+ }
107
+
108
+ // ── Window lifecycle ──────────────────────────────────────────────────
109
+ const app = new Application({ controlFlow: ControlFlow.Wait })
110
+ let win: ReturnType<typeof app.createBrowserWindow> | null = null
111
+ let webview: ReturnType<NonNullable<typeof win>['createWebview']> | null = null
112
+
113
+ app.onEvent((event) => {
114
+ const kind = event && ((event as any).kind || (event as any).event)
115
+ if (kind === 'window-close-requested') {
116
+ win = null
117
+ webview = null
118
+ printClosed()
119
+ }
120
+ })
121
+
122
+ async function openWindow() {
123
+ if (win) { printHint('Window is already open'); return }
124
+ win = app.createBrowserWindow({
125
+ title: WIN_TITLE,
126
+ width: WIN_SIZE.width,
127
+ height: WIN_SIZE.height,
128
+ })
129
+ webview = win.createWebview({ html: await renderApp() })
130
+ printOpened()
131
+ }
132
+
133
+ function closeWindow() {
134
+ if (!win) return
135
+ try { win.dispose() } catch {}
136
+ win = null
137
+ webview = null
138
+ }
139
+
140
+ async function reload(triggerFile: string) {
141
+ if (!webview) return
142
+ try {
143
+ webview.loadHtml(await renderApp())
144
+ printReloaded(triggerFile.replace(projectRoot + '/', ''))
145
+ } catch (e: any) {
146
+ printError(`Reload failed: ${e.message}`)
147
+ }
148
+ }
149
+
150
+ // ── File watcher (HMR for src/) ───────────────────────────────────────
151
+ function setupHmr() {
152
+ if (!existsSync(SRC_DIR)) {
153
+ printHint('src/ directory not found — nothing to watch')
154
+ return
155
+ }
156
+ let debounce: NodeJS.Timeout | null = null
157
+ let lastFile = ''
158
+ try {
159
+ watch(SRC_DIR, { recursive: true }, (_event, filename) => {
160
+ if (!filename) return
161
+ if (debounce) clearTimeout(debounce)
162
+ lastFile = filename
163
+ debounce = setTimeout(() => { reload(lastFile) }, 80)
164
+ })
165
+ } catch (e: any) {
166
+ printHint(`HMR watcher failed: ${e.message}`)
167
+ }
168
+ }
169
+
170
+ // ── Keyboard shortcuts ────────────────────────────────────────────────
171
+ function setupShortcuts() {
172
+ if (!process.stdin.isTTY) return
173
+ process.stdin.setRawMode(true)
174
+ process.stdin.resume()
175
+ process.stdin.setEncoding('utf8')
176
+ process.stdin.on('data', (key: string) => {
177
+ if (key === 'o' || key === 'O') {
178
+ openWindow()
179
+ } else if (key === 'r' || key === 'R') {
180
+ closeWindow()
181
+ openWindow()
182
+ } else if (key === 'q' || key === 'Q' || key === '' /* Ctrl+C */) {
183
+ printBye()
184
+ try { process.stdin.setRawMode(false) } catch {}
185
+ try { app.exit() } catch {}
186
+ process.exit(0)
187
+ }
188
+ })
189
+ }
190
+
191
+ // ── Boot ──────────────────────────────────────────────────────────────
192
+ printBanner()
193
+ printShortcuts()
194
+ printStarting()
195
+ await openWindow()
196
+ printReady(Date.now() - START_AT)
197
+ setupShortcuts()
198
+ setupHmr()
199
+
200
+ app.run()