murasaki 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "murasaki",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "The desktop framework for Next.js developers. Node-powered. WebView-thin. No Rust. No Chromium.",
5
5
  "keywords": [
6
6
  "desktop",
@@ -24,7 +24,13 @@
24
24
  "author": "ichi",
25
25
  "type": "module",
26
26
  "bin": {
27
- "murasaki": "./bin/murasaki.mjs"
27
+ "murasaki": "./bin/murasaki.js"
28
+ },
29
+ "exports": {
30
+ ".": {
31
+ "types": "./src/index.ts",
32
+ "default": "./src/index.ts"
33
+ }
28
34
  },
29
35
  "files": [
30
36
  "bin",
@@ -33,7 +39,6 @@
33
39
  "LICENSE"
34
40
  ],
35
41
  "scripts": {
36
- "example:hello": "MURASAKI_DEV=1 tsx examples/hello/index.tsx",
37
42
  "example:app-router": "cd examples/app-router && pnpm dev"
38
43
  },
39
44
  "dependencies": {
package/src/dev.tsx CHANGED
@@ -27,9 +27,10 @@ const c = (code: string) => (noColor ? '' : code)
27
27
 
28
28
  // ── Project paths ─────────────────────────────────────────────────────
29
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')
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
+ const GLOBALS_CSS = join(SRC_DIR, 'globals.css')
33
34
 
34
35
  // ── Resolve murasaki version (for the banner) ─────────────────────────
35
36
  const __dirname = dirname(fileURLToPath(import.meta.url))
@@ -48,17 +49,21 @@ const WEBVIEW_ENGINE = (() => {
48
49
  }
49
50
  })()
50
51
 
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
52
+ const DEFAULT_WIN_TITLE = 'Murasaki App'
53
+ const DEFAULT_WIN_SIZE = { width: 1280, height: 800 }
54
+ const START_AT = Date.now()
55
+ const isDev = process.env.MURASAKI_DEV === '1' || true // dev runner always = dev
56
+
57
+ // Active window config (mutated when metadata is read)
58
+ let winTitle = DEFAULT_WIN_TITLE
59
+ let winSize = { ...DEFAULT_WIN_SIZE }
55
60
 
56
61
  // ── Banner ────────────────────────────────────────────────────────────
57
62
  function printBanner() {
58
63
  process.stdout.write('\n')
59
64
  process.stdout.write(` ${c(BOLD)}${c(BRIGHT)}🦋 Murasaki${c(RESET)} ${c(DIM)}${VERSION}${c(RESET)}\n\n`)
60
65
  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`)
66
+ process.stdout.write(` ${c(DIM)}-${c(RESET)} ${c(DIM)}Window ${c(RESET)}${winTitle} ${c(DIM)}(${winSize.width}×${winSize.height})${c(RESET)}\n`)
62
67
  process.stdout.write(` ${c(DIM)}-${c(RESET)} ${c(DIM)}Webview ${c(RESET)}${WEBVIEW_ENGINE}\n`)
63
68
  process.stdout.write(` ${c(DIM)}-${c(RESET)} ${c(DIM)}Runtime ${c(RESET)}Node ${process.version}\n`)
64
69
  process.stdout.write(` ${c(DIM)}-${c(RESET)} ${c(DIM)}Mode ${c(RESET)}development ${c(DIM)}(HMR active)${c(RESET)}\n\n`)
@@ -76,6 +81,14 @@ function printError(msg: string) { process.stdout.write(` ${c(RED)}${c(BOLD)}✗
76
81
  // ── Routing: load app/page.tsx (+ optional app/layout.tsx) ────────────
77
82
  type ReactComponent = ComponentType<{ children?: ReactNode }>
78
83
 
84
+ type AppMetadata = {
85
+ title?: string
86
+ description?: string
87
+ window?: { title?: string; width?: number; height?: number }
88
+ }
89
+
90
+ type LayoutModule = { component: ReactComponent; metadata?: AppMetadata } | null
91
+
79
92
  async function dynImport(path: string) {
80
93
  // Cache-bust so file edits are picked up without restarting the process.
81
94
  const url = pathToFileURL(path).href + `?v=${Date.now()}`
@@ -88,21 +101,58 @@ async function loadApp(): Promise<ReactComponent | null> {
88
101
  return mod.default as ReactComponent
89
102
  }
90
103
 
91
- async function loadLayout(): Promise<ReactComponent | null> {
104
+ async function loadLayout(): Promise<LayoutModule> {
92
105
  if (!existsSync(LAYOUT_PATH)) return null
93
106
  const mod = await dynImport(LAYOUT_PATH)
94
- return mod.default as ReactComponent
107
+ if (!mod.default) return null
108
+ return { component: mod.default as ReactComponent, metadata: mod.metadata as AppMetadata | undefined }
95
109
  }
96
110
 
97
- async function renderApp(): Promise<string> {
111
+ function loadGlobalsCss(): string {
112
+ if (!existsSync(GLOBALS_CSS)) return ''
113
+ try { return readFileSync(GLOBALS_CSS, 'utf8') } catch { return '' }
114
+ }
115
+
116
+ function escapeHtml(s: string) {
117
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
118
+ }
119
+
120
+ async function renderApp(): Promise<{ html: string; metadata?: AppMetadata }> {
98
121
  const App = await loadApp()
99
122
  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>'
123
+ return { html: '<!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
124
  }
102
- const Layout = await loadLayout()
125
+ const layoutData = await loadLayout()
126
+ const metadata = layoutData?.metadata
103
127
  const appEl = createElement(App)
104
- const tree = Layout ? createElement(Layout, null, appEl) : appEl
105
- return '<!doctype html>' + renderToStaticMarkup(tree)
128
+ const tree = layoutData ? createElement(layoutData.component, null, appEl) : appEl
129
+ let html = '<!doctype html>' + renderToStaticMarkup(tree)
130
+
131
+ // Inject <title> + <meta description> from metadata if not present in head
132
+ const headInjects: string[] = []
133
+ if (metadata?.title && !/<title>.*?<\/title>/i.test(html)) {
134
+ headInjects.push(`<title>${escapeHtml(metadata.title)}</title>`)
135
+ }
136
+ if (metadata?.description && !/<meta[^>]+name=["']description["']/i.test(html)) {
137
+ headInjects.push(`<meta name="description" content="${escapeHtml(metadata.description)}">`)
138
+ }
139
+
140
+ // Inject src/globals.css (auto, like Next.js's import './globals.css')
141
+ const css = loadGlobalsCss()
142
+ if (css) {
143
+ headInjects.push(`<style data-murasaki="globals.css">${css}</style>`)
144
+ }
145
+
146
+ if (headInjects.length) {
147
+ const blob = headInjects.join('')
148
+ if (html.includes('</head>')) {
149
+ html = html.replace('</head>', blob + '</head>')
150
+ } else {
151
+ html = html.replace('<body', blob + '<body')
152
+ }
153
+ }
154
+
155
+ return { html, metadata }
106
156
  }
107
157
 
108
158
  // ── Window lifecycle ──────────────────────────────────────────────────
@@ -113,20 +163,36 @@ let webview: ReturnType<NonNullable<typeof win>['createWebview']> | null = null
113
163
  app.onEvent((event) => {
114
164
  const kind = event && ((event as any).kind || (event as any).event)
115
165
  if (kind === 'window-close-requested') {
166
+ // Dispose the window so the OS close completes — without this the
167
+ // close button hangs / appears frozen.
168
+ if (win) {
169
+ try { win.dispose() } catch {}
170
+ }
116
171
  win = null
117
172
  webview = null
118
173
  printClosed()
119
174
  }
120
175
  })
121
176
 
177
+ function applyMetadataToWindowConfig(metadata?: AppMetadata) {
178
+ if (!metadata) return
179
+ if (metadata.window?.title) winTitle = metadata.window.title
180
+ else if (metadata.title) winTitle = metadata.title
181
+ if (metadata.window?.width) winSize.width = metadata.window.width
182
+ if (metadata.window?.height) winSize.height = metadata.window.height
183
+ }
184
+
122
185
  async function openWindow() {
123
186
  if (win) { printHint('Window is already open'); return }
187
+ // Render first so we can read metadata and apply window config
188
+ const { html, metadata } = await renderApp()
189
+ applyMetadataToWindowConfig(metadata)
124
190
  win = app.createBrowserWindow({
125
- title: WIN_TITLE,
126
- width: WIN_SIZE.width,
127
- height: WIN_SIZE.height,
191
+ title: winTitle,
192
+ width: winSize.width,
193
+ height: winSize.height,
128
194
  })
129
- webview = win.createWebview({ html: await renderApp() })
195
+ webview = win.createWebview({ html })
130
196
  printOpened()
131
197
  }
132
198
 
@@ -140,7 +206,8 @@ function closeWindow() {
140
206
  async function reload(triggerFile: string) {
141
207
  if (!webview) return
142
208
  try {
143
- webview.loadHtml(await renderApp())
209
+ const { html } = await renderApp()
210
+ webview.loadHtml(html)
144
211
  printReloaded(triggerFile.replace(projectRoot + '/', ''))
145
212
  } catch (e: any) {
146
213
  printError(`Reload failed: ${e.message}`)
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ // murasaki — public API
2
+ //
3
+ // Import like:
4
+ // import type { Metadata } from 'murasaki'
5
+
6
+ export type Metadata = {
7
+ /** Default <title> for the app (overridden by <title> tag inside <head>) */
8
+ title?: string
9
+ /** <meta name="description"> */
10
+ description?: string
11
+ /** Initial window options (applied at first open; user can resize after) */
12
+ window?: {
13
+ /** Window title bar text (defaults to metadata.title) */
14
+ title?: string
15
+ /** Initial width in logical pixels */
16
+ width?: number
17
+ /** Initial height in logical pixels */
18
+ height?: number
19
+ }
20
+ }
File without changes