murasaki 0.2.0 → 0.3.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.
Files changed (98) hide show
  1. package/bin/murasaki.js +8 -3
  2. package/dist/cli/colors.d.ts +13 -0
  3. package/dist/cli/colors.d.ts.map +1 -0
  4. package/dist/cli/colors.js +14 -0
  5. package/dist/cli/colors.js.map +1 -0
  6. package/dist/cli/log.d.ts +14 -0
  7. package/dist/cli/log.d.ts.map +1 -0
  8. package/dist/cli/log.js +25 -0
  9. package/dist/cli/log.js.map +1 -0
  10. package/dist/components/Link.d.ts +9 -0
  11. package/dist/components/Link.d.ts.map +1 -0
  12. package/dist/components/Link.js +15 -0
  13. package/dist/components/Link.js.map +1 -0
  14. package/dist/dev.d.ts +2 -0
  15. package/dist/dev.d.ts.map +1 -0
  16. package/dist/dev.js +41 -0
  17. package/dist/dev.js.map +1 -0
  18. package/dist/env.d.ts +15 -0
  19. package/dist/env.d.ts.map +1 -0
  20. package/dist/env.js +43 -0
  21. package/dist/env.js.map +1 -0
  22. package/dist/index.d.ts +18 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +7 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/jsx/dom/index.d.ts +2 -0
  27. package/dist/jsx/dom/index.d.ts.map +1 -0
  28. package/dist/jsx/dom/index.js +3 -0
  29. package/dist/jsx/dom/index.js.map +1 -0
  30. package/dist/jsx/dom/runtime.d.ts +34 -0
  31. package/dist/jsx/dom/runtime.d.ts.map +1 -0
  32. package/dist/jsx/dom/runtime.js +303 -0
  33. package/dist/jsx/dom/runtime.js.map +1 -0
  34. package/dist/jsx/index.d.ts +3 -0
  35. package/dist/jsx/index.d.ts.map +1 -0
  36. package/dist/jsx/index.js +3 -0
  37. package/dist/jsx/index.js.map +1 -0
  38. package/dist/jsx/jsx-dev-runtime.d.ts +2 -0
  39. package/dist/jsx/jsx-dev-runtime.d.ts.map +1 -0
  40. package/dist/jsx/jsx-dev-runtime.js +7 -0
  41. package/dist/jsx/jsx-dev-runtime.js.map +1 -0
  42. package/dist/jsx/jsx-runtime.d.ts +2 -0
  43. package/dist/jsx/jsx-runtime.d.ts.map +1 -0
  44. package/{src/jsx/jsx-runtime.ts → dist/jsx/jsx-runtime.js} +4 -3
  45. package/dist/jsx/jsx-runtime.js.map +1 -0
  46. package/dist/jsx/runtime.d.ts +24 -0
  47. package/dist/jsx/runtime.d.ts.map +1 -0
  48. package/dist/jsx/runtime.js +273 -0
  49. package/dist/jsx/runtime.js.map +1 -0
  50. package/dist/jsx/types.d.ts +30 -0
  51. package/dist/jsx/types.d.ts.map +1 -0
  52. package/dist/jsx/types.js +3 -0
  53. package/dist/jsx/types.js.map +1 -0
  54. package/dist/runtime/bundle.d.ts +8 -0
  55. package/dist/runtime/bundle.d.ts.map +1 -0
  56. package/dist/runtime/bundle.js +98 -0
  57. package/dist/runtime/bundle.js.map +1 -0
  58. package/dist/runtime/hmr.d.ts +2 -0
  59. package/dist/runtime/hmr.d.ts.map +1 -0
  60. package/dist/runtime/hmr.js +28 -0
  61. package/dist/runtime/hmr.js.map +1 -0
  62. package/dist/runtime/render.d.ts +3 -0
  63. package/dist/runtime/render.d.ts.map +1 -0
  64. package/dist/runtime/render.js +226 -0
  65. package/dist/runtime/render.js.map +1 -0
  66. package/dist/runtime/routes.d.ts +10 -0
  67. package/dist/runtime/routes.d.ts.map +1 -0
  68. package/dist/runtime/routes.js +65 -0
  69. package/dist/runtime/routes.js.map +1 -0
  70. package/dist/runtime/shortcuts.d.ts +8 -0
  71. package/dist/runtime/shortcuts.d.ts.map +1 -0
  72. package/dist/runtime/shortcuts.js +29 -0
  73. package/dist/runtime/shortcuts.js.map +1 -0
  74. package/dist/runtime/window.d.ts +10 -0
  75. package/dist/runtime/window.d.ts.map +1 -0
  76. package/dist/runtime/window.js +93 -0
  77. package/dist/runtime/window.js.map +1 -0
  78. package/dist/types.d.ts +17 -0
  79. package/dist/types.d.ts.map +1 -0
  80. package/dist/types.js +3 -0
  81. package/dist/types.js.map +1 -0
  82. package/package.json +17 -10
  83. package/src/cli/colors.ts +0 -16
  84. package/src/cli/log.ts +0 -42
  85. package/src/components/Link.tsx +0 -25
  86. package/src/dev.tsx +0 -60
  87. package/src/env.ts +0 -48
  88. package/src/index.ts +0 -24
  89. package/src/jsx/index.ts +0 -21
  90. package/src/jsx/jsx-dev-runtime.ts +0 -6
  91. package/src/jsx/runtime.ts +0 -298
  92. package/src/jsx/types.ts +0 -36
  93. package/src/runtime/hmr.ts +0 -26
  94. package/src/runtime/render.tsx +0 -225
  95. package/src/runtime/routes.ts +0 -73
  96. package/src/runtime/shortcuts.ts +0 -31
  97. package/src/runtime/window.ts +0 -94
  98. package/src/types.ts +0 -22
@@ -1,225 +0,0 @@
1
- // Multi-route renderer.
2
- //
3
- // Pipeline:
4
- // 1. discover all src/app/<...>/page.tsx
5
- // 2. render each page wrapped in its nested layouts (NOT the root layout)
6
- // 3. render the root layout once with a switcher block as its children
7
- // 4. inject metadata (<title>, <meta description>) + globals.css into <head>
8
- // 5. inject a tiny navigation script that listens to hash changes and
9
- // intercepts <Link> clicks
10
- //
11
- // Legacy single-page (src/app.tsx + src/layout.tsx) is still supported:
12
- // if src/app/ doesn't exist, we render the legacy convention.
13
-
14
- import { existsSync, readFileSync } from 'node:fs'
15
- import { pathToFileURL } from 'node:url'
16
- import {
17
- APP_DIR,
18
- APP_GLOBALS_CSS,
19
- LEGACY_APP_PATH,
20
- LEGACY_GLOBALS_CSS,
21
- LEGACY_LAYOUT_PATH,
22
- } from '../env.ts'
23
- import type { Metadata } from '../index.ts'
24
- import { jsx, raw, renderToString } from '../jsx/runtime.ts'
25
- import type { Child, Component } from '../jsx/types.ts'
26
- import type { RenderResult } from '../types.ts'
27
- import { discoverRoutes, type Route } from './routes.ts'
28
-
29
- const NO_APP_HTML =
30
- '<!doctype html><html><body style="font-family:system-ui;padding:40px;">' +
31
- '<h1 style="color:#A855F7">No app found</h1>' +
32
- '<p>Create <code>src/app/page.tsx</code> and the window will reload.</p></body></html>'
33
-
34
- function escapeHtml(s: string): string {
35
- return s
36
- .replace(/&/g, '&amp;')
37
- .replace(/</g, '&lt;')
38
- .replace(/>/g, '&gt;')
39
- .replace(/"/g, '&quot;')
40
- }
41
-
42
- async function dynImport(path: string) {
43
- const url = pathToFileURL(path).href + `?v=${Date.now()}`
44
- return import(url)
45
- }
46
-
47
- type Loaded = {
48
- default: Component
49
- metadata?: Metadata
50
- }
51
-
52
- async function loadModule(path: string): Promise<Loaded | null> {
53
- if (!existsSync(path)) return null
54
- const mod = await dynImport(path)
55
- if (!mod.default) return null
56
- return mod
57
- }
58
-
59
- // ── Page rendering ──────────────────────────────────────────────────
60
- async function renderPageInner(route: Route, rootLayoutFile: string | null): Promise<string> {
61
- const pageMod = await loadModule(route.pageFile)
62
- if (!pageMod) return ''
63
- let tree: Child = jsx(pageMod.default, null)
64
-
65
- // Wrap in nested layouts, innermost first (i.e. skip root layout — it wraps everything later)
66
- // route.layoutFiles: outermost first → so iterate from end backwards excluding root
67
- const layoutsToApply = route.layoutFiles.filter((f) => f !== rootLayoutFile)
68
- // Apply from innermost (last in array) outward (first in array) so the
69
- // outer layout wraps the inner: <Outer><Inner><Page/></Inner></Outer>
70
- for (let i = layoutsToApply.length - 1; i >= 0; i--) {
71
- const layoutMod = await loadModule(layoutsToApply[i])
72
- if (!layoutMod) continue
73
- tree = jsx(layoutMod.default, { children: tree })
74
- }
75
- return renderToString(tree)
76
- }
77
-
78
- // ── Root layout + metadata ──────────────────────────────────────────
79
- async function renderRootLayout(rootLayout: Loaded | null, body: Child): Promise<string> {
80
- if (!rootLayout) {
81
- // Fallback root if user didn't write src/app/layout.tsx
82
- const fallback = jsx('html', {
83
- lang: 'en',
84
- children: [
85
- jsx('head', { children: jsx('meta', { charSet: 'utf-8' }) }),
86
- jsx('body', { children: body }),
87
- ],
88
- })
89
- return renderToString(fallback)
90
- }
91
- const tree = jsx(rootLayout.default, { children: body })
92
- return renderToString(tree)
93
- }
94
-
95
- // ── Navigation script (injected once) ───────────────────────────────
96
- const NAV_SCRIPT = `
97
- <script>
98
- (function(){
99
- function showRoute(path){
100
- var blocks=document.querySelectorAll('[data-murasaki-route]');
101
- var matched=false;
102
- for(var i=0;i<blocks.length;i++){
103
- var b=blocks[i];
104
- if(b.getAttribute('data-murasaki-route')===path){
105
- b.removeAttribute('hidden');matched=true;
106
- } else {
107
- b.setAttribute('hidden','');
108
- }
109
- }
110
- if(!matched){
111
- // fallback to "/"
112
- var root=document.querySelector('[data-murasaki-route="/"]');
113
- if(root)root.removeAttribute('hidden');
114
- }
115
- document.dispatchEvent(new CustomEvent('murasaki:navigate',{detail:{path:path}}));
116
- }
117
- function currentPath(){
118
- var h=location.hash||'';
119
- return h.charAt(0)==='#'?h.slice(1)||'/':'/'
120
- }
121
- window.addEventListener('hashchange',function(){showRoute(currentPath())});
122
- document.addEventListener('click',function(e){
123
- var t=e.target;
124
- while(t&&t.nodeType===1){
125
- if(t.tagName==='A'&&t.hasAttribute('data-murasaki-link')){
126
- e.preventDefault();
127
- var href=t.getAttribute('data-murasaki-link');
128
- if('#'+href!==location.hash){location.hash=href;}
129
- else{showRoute(href);}
130
- return;
131
- }
132
- t=t.parentNode;
133
- }
134
- });
135
- // Initial render
136
- showRoute(currentPath());
137
- })();
138
- </script>
139
- `.trim()
140
-
141
- // ── Globals.css discovery (app/ takes precedence over src/) ─────────
142
- function loadGlobalsCss(): string {
143
- for (const p of [APP_GLOBALS_CSS, LEGACY_GLOBALS_CSS]) {
144
- if (existsSync(p)) {
145
- try {
146
- return readFileSync(p, 'utf8')
147
- } catch {}
148
- }
149
- }
150
- return ''
151
- }
152
-
153
- // ── Head injection ──────────────────────────────────────────────────
154
- function injectHead(html: string, metadata: Metadata | undefined, css: string): string {
155
- const headInjects: string[] = []
156
- if (metadata?.title && !/<title>.*?<\/title>/i.test(html)) {
157
- headInjects.push(`<title>${escapeHtml(metadata.title)}</title>`)
158
- }
159
- if (metadata?.description && !/<meta[^>]+name=["']description["']/i.test(html)) {
160
- headInjects.push(`<meta name="description" content="${escapeHtml(metadata.description)}">`)
161
- }
162
- if (css) {
163
- headInjects.push(`<style data-murasaki="globals.css">${css}</style>`)
164
- }
165
- if (!headInjects.length) return html
166
- const blob = headInjects.join('')
167
- if (html.includes('</head>')) return html.replace('</head>', blob + '</head>')
168
- return html.replace('<body', blob + '<body')
169
- }
170
-
171
- // ── Main entry ──────────────────────────────────────────────────────
172
- export async function renderApp(): Promise<RenderResult> {
173
- // 1. Try app-router convention first
174
- if (existsSync(APP_DIR)) {
175
- return renderAppRouter()
176
- }
177
- // 2. Fall back to legacy single-page
178
- if (existsSync(LEGACY_APP_PATH)) {
179
- return renderLegacy()
180
- }
181
- return { html: NO_APP_HTML }
182
- }
183
-
184
- async function renderAppRouter(): Promise<RenderResult> {
185
- const routes = discoverRoutes(APP_DIR)
186
- if (routes.length === 0) return { html: NO_APP_HTML }
187
-
188
- // Identify root layout (src/app/layout.tsx) if any
189
- const rootLayoutPath = routes[0]?.layoutFiles[0]
190
- const rootLayoutFile =
191
- rootLayoutPath && rootLayoutPath.endsWith('/app/layout.tsx') ? rootLayoutPath : null
192
- const rootLayoutMod = rootLayoutFile ? await loadModule(rootLayoutFile) : null
193
- const metadata = rootLayoutMod?.metadata
194
-
195
- // 2. Render each page (wrapped in its nested layouts, excluding root)
196
- const blocks: string[] = []
197
- for (const route of routes) {
198
- const inner = await renderPageInner(route, rootLayoutFile)
199
- blocks.push(`<div data-murasaki-route="${escapeHtml(route.path)}" hidden>${inner}</div>`)
200
- }
201
- const switcher = raw(blocks.join('') + NAV_SCRIPT)
202
-
203
- // 3. Render root layout with switcher as children
204
- const bodyContent = await renderRootLayout(rootLayoutMod, switcher)
205
- let html = '<!doctype html>' + bodyContent
206
-
207
- // 4. Inject metadata + globals.css
208
- html = injectHead(html, metadata, loadGlobalsCss())
209
-
210
- return { html, metadata }
211
- }
212
-
213
- // ── Legacy single-page render (unchanged shape) ─────────────────────
214
- async function renderLegacy(): Promise<RenderResult> {
215
- const pageMod = await loadModule(LEGACY_APP_PATH)
216
- if (!pageMod) return { html: NO_APP_HTML }
217
- const layoutMod = await loadModule(LEGACY_LAYOUT_PATH)
218
- const metadata = layoutMod?.metadata
219
- const body: Child = layoutMod
220
- ? jsx(layoutMod.default, { children: jsx(pageMod.default, null) })
221
- : jsx(pageMod.default, null)
222
- let html = '<!doctype html>' + renderToString(body)
223
- html = injectHead(html, metadata, loadGlobalsCss())
224
- return { html, metadata }
225
- }
@@ -1,73 +0,0 @@
1
- // File-based route discovery for src/app/.
2
- //
3
- // Conventions (subset of Next.js app router):
4
- // src/app/page.tsx → "/"
5
- // src/app/layout.tsx → root layout (wraps everything)
6
- // src/app/about/page.tsx → "/about"
7
- // src/app/about/layout.tsx → nested layout for /about and its children
8
- // src/app/_foo/ → ignored (leading underscore = private)
9
- //
10
- // A page.tsx is required to register a route. A layout.tsx without a
11
- // page.tsx in the same dir just contributes to children's layouts.
12
-
13
- import { existsSync, readdirSync, statSync } from 'node:fs'
14
- import { join } from 'node:path'
15
-
16
- export type Route = {
17
- /** URL-style path: "/", "/about", "/settings/profile" */
18
- path: string
19
- /** Absolute path to the page.tsx file */
20
- pageFile: string
21
- /** Absolute paths to layout.tsx files, OUTERMOST first (root → innermost) */
22
- layoutFiles: string[]
23
- }
24
-
25
- export function discoverRoutes(appDir: string): Route[] {
26
- if (!existsSync(appDir)) return []
27
- const out: Route[] = []
28
- walk(appDir, '', [], out)
29
- // Stable sort so "/" comes first, then alphabetical
30
- out.sort((a, b) => {
31
- if (a.path === '/') return -1
32
- if (b.path === '/') return 1
33
- return a.path.localeCompare(b.path)
34
- })
35
- return out
36
- }
37
-
38
- function walk(dir: string, routePath: string, layouts: string[], out: Route[]): void {
39
- let entries: string[]
40
- try {
41
- entries = readdirSync(dir)
42
- } catch {
43
- return
44
- }
45
-
46
- const hasLayout = entries.includes('layout.tsx')
47
- const hasPage = entries.includes('page.tsx')
48
-
49
- // The current directory contributes its layout to itself and descendants.
50
- const ownLayouts = hasLayout ? [...layouts, join(dir, 'layout.tsx')] : layouts
51
-
52
- if (hasPage) {
53
- out.push({
54
- path: routePath || '/',
55
- pageFile: join(dir, 'page.tsx'),
56
- layoutFiles: ownLayouts,
57
- })
58
- }
59
-
60
- // Recurse into subdirectories (skip files, hidden, private "_*", node_modules).
61
- for (const entry of entries) {
62
- if (entry.startsWith('.') || entry.startsWith('_') || entry === 'node_modules') continue
63
- const full = join(dir, entry)
64
- let isDir = false
65
- try {
66
- isDir = statSync(full).isDirectory()
67
- } catch {
68
- continue
69
- }
70
- if (!isDir) continue
71
- walk(full, `${routePath}/${entry}`, ownLayouts, out)
72
- }
73
- }
@@ -1,31 +0,0 @@
1
- // Terminal keyboard shortcuts (raw mode stdin).
2
- //
3
- // o open the window
4
- // r restart (close + open)
5
- // q quit
6
- // Ctrl+C quit
7
-
8
- export type ShortcutHandlers = {
9
- onOpen: () => void
10
- onRestart: () => void
11
- onQuit: () => void
12
- }
13
-
14
- export function setupShortcuts(handlers: ShortcutHandlers): void {
15
- if (!process.stdin.isTTY) return
16
- process.stdin.setRawMode(true)
17
- process.stdin.resume()
18
- process.stdin.setEncoding('utf8')
19
- process.stdin.on('data', (key) => {
20
- const k = key.toString()
21
- if (k === 'o' || k === 'O') handlers.onOpen()
22
- else if (k === 'r' || k === 'R') handlers.onRestart()
23
- else if (k === 'q' || k === 'Q' || k === '\x03' /* Ctrl+C */) handlers.onQuit()
24
- })
25
- }
26
-
27
- export function teardownStdin(): void {
28
- try {
29
- process.stdin.setRawMode(false)
30
- } catch {}
31
- }
@@ -1,94 +0,0 @@
1
- // Owns the Application + BrowserWindow lifecycle.
2
- //
3
- // The EventLoop is created exactly once (winit/tao limitation). ControlFlow.Wait
4
- // keeps the loop alive even when no windows are open, so the user can press
5
- // `o` in the terminal to re-open the window.
6
-
7
- import { Application, ControlFlow } from '@webviewjs/webview'
8
- import { printClosed, printError, printHint, printReloaded } from '../cli/log.ts'
9
- import { DEFAULT_WIN_SIZE, DEFAULT_WIN_TITLE, projectRoot } from '../env.ts'
10
- import type { Metadata } from '../index.ts'
11
- import type { WindowConfig } from '../types.ts'
12
- import { renderApp } from './render.tsx'
13
-
14
- const config: WindowConfig = {
15
- title: DEFAULT_WIN_TITLE,
16
- width: DEFAULT_WIN_SIZE.width,
17
- height: DEFAULT_WIN_SIZE.height,
18
- }
19
-
20
- export const app = new Application({ controlFlow: ControlFlow.Wait })
21
- let win: ReturnType<typeof app.createBrowserWindow> | null = null
22
- let webview: ReturnType<NonNullable<typeof win>['createWebview']> | null = null
23
-
24
- app.onEvent((event) => {
25
- const kind = event && ((event as any).kind || (event as any).event)
26
- if (kind === 'window-close-requested') {
27
- // Dispose so the OS close completes — without this the close button hangs.
28
- if (win) {
29
- try {
30
- win.dispose()
31
- } catch {}
32
- }
33
- win = null
34
- webview = null
35
- printClosed()
36
- }
37
- })
38
-
39
- function applyMetadata(metadata?: Metadata) {
40
- if (!metadata) return
41
- if (metadata.window?.title) config.title = metadata.window.title
42
- else if (metadata.title) config.title = metadata.title
43
- if (metadata.window?.width) config.width = metadata.window.width
44
- if (metadata.window?.height) config.height = metadata.window.height
45
- }
46
-
47
- export function getConfig(): Readonly<WindowConfig> {
48
- return config
49
- }
50
-
51
- export async function openWindow(): Promise<void> {
52
- if (win) {
53
- printHint('Window is already open')
54
- return
55
- }
56
- const { html, metadata } = await renderApp()
57
- applyMetadata(metadata)
58
- win = app.createBrowserWindow({
59
- title: config.title,
60
- width: config.width,
61
- height: config.height,
62
- })
63
- webview = win.createWebview({ html })
64
- }
65
-
66
- export function closeWindow(): void {
67
- if (!win) return
68
- try {
69
- win.dispose()
70
- } catch {}
71
- win = null
72
- webview = null
73
- }
74
-
75
- export async function reloadWindow(triggerFile: string): Promise<void> {
76
- if (!webview) return
77
- try {
78
- const { html } = await renderApp()
79
- webview.loadHtml(html)
80
- printReloaded(triggerFile.replace(projectRoot + '/', ''))
81
- } catch (e: any) {
82
- printError(`Reload failed: ${e.message}`)
83
- }
84
- }
85
-
86
- export function runApp(): void {
87
- app.run()
88
- }
89
-
90
- export function exitApp(): void {
91
- try {
92
- app.exit()
93
- } catch {}
94
- }
package/src/types.ts DELETED
@@ -1,22 +0,0 @@
1
- // Internal shared types (the public Metadata type is re-exported from index.ts).
2
-
3
- import type { Metadata } from './index.ts'
4
- import type { Component } from './jsx/types.ts'
5
-
6
- export type AppComponent = Component
7
-
8
- export type LayoutModule = {
9
- component: AppComponent
10
- metadata?: Metadata
11
- } | null
12
-
13
- export type WindowConfig = {
14
- title: string
15
- width: number
16
- height: number
17
- }
18
-
19
- export type RenderResult = {
20
- html: string
21
- metadata?: Metadata
22
- }