@emkodev/emroute 1.0.3 → 1.6.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 (46) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +147 -12
  3. package/package.json +48 -7
  4. package/runtime/abstract.runtime.ts +441 -0
  5. package/runtime/bun/esbuild-runtime-loader.plugin.ts +94 -0
  6. package/runtime/bun/fs/bun-fs.runtime.ts +245 -0
  7. package/runtime/bun/sqlite/bun-sqlite.runtime.ts +279 -0
  8. package/runtime/sitemap.generator.ts +180 -0
  9. package/server/codegen.util.ts +66 -0
  10. package/server/emroute.server.ts +398 -0
  11. package/server/esbuild-manifest.plugin.ts +243 -0
  12. package/server/scanner.util.ts +243 -0
  13. package/server/server-api.type.ts +90 -0
  14. package/src/component/abstract.component.ts +229 -0
  15. package/src/component/page.component.ts +134 -0
  16. package/src/component/widget.component.ts +85 -0
  17. package/src/element/component.element.ts +353 -0
  18. package/src/element/markdown.element.ts +107 -0
  19. package/src/element/slot.element.ts +31 -0
  20. package/src/index.ts +61 -0
  21. package/src/overlay/mod.ts +10 -0
  22. package/src/overlay/overlay.css.ts +170 -0
  23. package/src/overlay/overlay.service.ts +348 -0
  24. package/src/overlay/overlay.type.ts +38 -0
  25. package/src/renderer/spa/base.renderer.ts +186 -0
  26. package/src/renderer/spa/hash.renderer.ts +215 -0
  27. package/src/renderer/spa/html.renderer.ts +382 -0
  28. package/src/renderer/spa/mod.ts +76 -0
  29. package/src/renderer/ssr/html.renderer.ts +159 -0
  30. package/src/renderer/ssr/md.renderer.ts +142 -0
  31. package/src/renderer/ssr/ssr.renderer.ts +286 -0
  32. package/src/route/route.core.ts +316 -0
  33. package/src/route/route.matcher.ts +260 -0
  34. package/src/type/logger.type.ts +24 -0
  35. package/src/type/markdown.type.ts +21 -0
  36. package/src/type/navigation-api.d.ts +95 -0
  37. package/src/type/route.type.ts +149 -0
  38. package/src/type/widget.type.ts +65 -0
  39. package/src/util/html.util.ts +186 -0
  40. package/src/util/logger.util.ts +83 -0
  41. package/src/util/widget-resolve.util.ts +197 -0
  42. package/src/web-doc/index.md +15 -0
  43. package/src/widget/breadcrumb.widget.ts +106 -0
  44. package/src/widget/page-title.widget.ts +52 -0
  45. package/src/widget/widget.parser.ts +89 -0
  46. package/src/widget/widget.registry.ts +51 -0
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Navigation API type declarations.
3
+ *
4
+ * The Navigation API is supported by all major browsers (Chrome 102+, Edge 102+,
5
+ * Firefox 147+, Safari 26.2+) but TypeScript's lib.dom.d.ts does not yet include
6
+ * these types. This file provides the subset used by the SPA renderer.
7
+ *
8
+ * Will be removed once TypeScript ships native Navigation API types.
9
+ *
10
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API
11
+ * @see ADR-0014
12
+ */
13
+
14
+ interface NavigationNavigateOptions {
15
+ state?: unknown;
16
+ history?: 'auto' | 'push' | 'replace';
17
+ }
18
+
19
+ interface NavigationResult {
20
+ committed: Promise<NavigationHistoryEntry>;
21
+ finished: Promise<NavigationHistoryEntry>;
22
+ }
23
+
24
+ interface NavigationHistoryEntry extends EventTarget {
25
+ readonly key: string;
26
+ readonly id: string;
27
+ readonly url: string | null;
28
+ readonly index: number;
29
+ readonly sameDocument: boolean;
30
+ getState(): unknown;
31
+ }
32
+
33
+ interface NavigationDestination {
34
+ readonly url: string;
35
+ readonly key: string | null;
36
+ readonly id: string | null;
37
+ readonly index: number;
38
+ readonly sameDocument: boolean;
39
+ getState(): unknown;
40
+ }
41
+
42
+ interface NavigationInterceptOptions {
43
+ handler?: () => Promise<void>;
44
+ focusReset?: 'after-transition' | 'manual';
45
+ scroll?: 'after-transition' | 'manual';
46
+ }
47
+
48
+ interface NavigateEvent extends Event {
49
+ readonly navigationType: 'push' | 'replace' | 'reload' | 'traverse';
50
+ readonly destination: NavigationDestination;
51
+ readonly canIntercept: boolean;
52
+ readonly userInitiated: boolean;
53
+ readonly hashChange: boolean;
54
+ readonly signal: AbortSignal;
55
+ readonly formData: FormData | null;
56
+ readonly downloadRequest: string | null;
57
+ readonly info: unknown;
58
+ intercept(options?: NavigationInterceptOptions): void;
59
+ scroll(): void;
60
+ }
61
+
62
+ interface NavigationUpdateCurrentEntryOptions {
63
+ state: unknown;
64
+ }
65
+
66
+ interface Navigation extends EventTarget {
67
+ readonly currentEntry: NavigationHistoryEntry | null;
68
+ readonly transition: NavigationTransition | null;
69
+ entries(): NavigationHistoryEntry[];
70
+ navigate(url: string, options?: NavigationNavigateOptions): NavigationResult;
71
+ reload(options?: NavigationNavigateOptions): NavigationResult;
72
+ back(): NavigationResult;
73
+ forward(): NavigationResult;
74
+ traverseTo(key: string): NavigationResult;
75
+ updateCurrentEntry(options: NavigationUpdateCurrentEntryOptions): void;
76
+ addEventListener(
77
+ type: 'navigate',
78
+ listener: (event: NavigateEvent) => void,
79
+ options?: AddEventListenerOptions,
80
+ ): void;
81
+ addEventListener(
82
+ type: 'navigatesuccess' | 'navigateerror' | 'currententrychange',
83
+ listener: (event: Event) => void,
84
+ options?: AddEventListenerOptions,
85
+ ): void;
86
+ }
87
+
88
+ interface NavigationTransition {
89
+ readonly navigationType: 'push' | 'replace' | 'reload' | 'traverse';
90
+ readonly from: NavigationHistoryEntry;
91
+ readonly finished: Promise<void>;
92
+ }
93
+
94
+ // deno-lint-ignore no-var
95
+ declare var navigation: Navigation;
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Router Types
3
+ *
4
+ * Native browser APIs only - no external dependencies.
5
+ * Follows islands architecture: pages = HTML, widgets = web components.
6
+ */
7
+
8
+ /** Parameters extracted from URL patterns */
9
+ export type RouteParams = Readonly<Record<string, string>>;
10
+
11
+ /** Immutable route context built once per navigation, shared across the render pipeline. */
12
+ export interface RouteInfo {
13
+ /** Actual URL path (e.g., '/projects/123') */
14
+ readonly pathname: string;
15
+
16
+ /** Matched route pattern (e.g., '/projects/:id') */
17
+ readonly pattern: string;
18
+
19
+ /** URL parameters extracted by the router */
20
+ readonly params: RouteParams;
21
+
22
+ /** Query string parameters */
23
+ readonly searchParams: URLSearchParams;
24
+ }
25
+
26
+ /** Supported file patterns in file-based routing */
27
+ export type RouteFileType = 'page' | 'error' | 'redirect';
28
+
29
+ /** Redirect configuration */
30
+ export interface RedirectConfig {
31
+ to: string;
32
+ status: 301 | 302;
33
+ }
34
+
35
+ /** Available files for a route */
36
+ export interface RouteFiles {
37
+ /** TypeScript module path (.page.ts) */
38
+ ts?: string;
39
+
40
+ /** HTML template path (.page.html) */
41
+ html?: string;
42
+
43
+ /** Markdown content path (.page.md) */
44
+ md?: string;
45
+
46
+ /** CSS stylesheet path (.page.css) */
47
+ css?: string;
48
+ }
49
+
50
+ /** Route configuration for a single route */
51
+ export interface RouteConfig {
52
+ /** URLPattern pathname pattern (e.g., '/projects/:id') */
53
+ pattern: string;
54
+
55
+ /** Type of route file */
56
+ type: RouteFileType;
57
+
58
+ /** Module path for dynamic import (primary file based on precedence) */
59
+ modulePath: string;
60
+
61
+ /** Available files for this route */
62
+ files?: RouteFiles;
63
+
64
+ /** Parent route pattern for nested routes */
65
+ parent?: string;
66
+
67
+ /** HTTP status code (for status-specific pages like 404, 401, 403) */
68
+ statusCode?: number;
69
+ }
70
+
71
+ /** Result of matching a URL against routes */
72
+ export interface MatchedRoute {
73
+ /** The matched route configuration */
74
+ readonly route: RouteConfig;
75
+
76
+ /** Extracted URL parameters */
77
+ readonly params: RouteParams;
78
+
79
+ /** Query string parameters from the matched URL */
80
+ readonly searchParams?: URLSearchParams;
81
+
82
+ /** The URLPatternResult from matching */
83
+ readonly patternResult?: URLPatternResult;
84
+ }
85
+
86
+ /** Error boundary configuration */
87
+ export interface ErrorBoundary {
88
+ /** Pattern prefix this error boundary handles */
89
+ pattern: string;
90
+
91
+ /** Module path for the error handler */
92
+ modulePath: string;
93
+ }
94
+
95
+ /** Generated routes manifest from build tool */
96
+ export interface RoutesManifest {
97
+ /** All page routes */
98
+ routes: RouteConfig[];
99
+
100
+ /** Error boundaries by pattern prefix */
101
+ errorBoundaries: ErrorBoundary[];
102
+
103
+ /** Status-specific pages (404, 401, 403) */
104
+ statusPages: Map<number, RouteConfig>;
105
+
106
+ /** Generic error handler */
107
+ errorHandler?: RouteConfig;
108
+
109
+ /** Pre-bundled module loaders keyed by module path (for SPA bundles) */
110
+ moduleLoaders?: Record<string, () => Promise<unknown>>;
111
+ }
112
+
113
+ /** Router state for history management */
114
+ export interface RouterState {
115
+ /** Current URL pathname */
116
+ pathname: string;
117
+
118
+ /** Extracted route parameters */
119
+ params: RouteParams;
120
+
121
+ /** Scroll position to restore */
122
+ scrollY?: number;
123
+ }
124
+
125
+ /** Navigation options */
126
+ export interface NavigateOptions {
127
+ /** Replace current history entry instead of pushing */
128
+ replace?: boolean;
129
+
130
+ /** State to store in history */
131
+ state?: RouterState;
132
+
133
+ /** Hash to scroll to after navigation */
134
+ hash?: string;
135
+ }
136
+
137
+ /** Router event types */
138
+ export type RouterEventType = 'navigate' | 'error' | 'load';
139
+
140
+ /** Router event payload */
141
+ export interface RouterEvent {
142
+ type: RouterEventType;
143
+ pathname: string;
144
+ params: RouteParams;
145
+ error?: Error;
146
+ }
147
+
148
+ /** Router event listener */
149
+ export type RouterEventListener = (event: RouterEvent) => void;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Widget System - Type Definitions
3
+ *
4
+ * Widgets are data-fetching components that work across three contexts:
5
+ * 1. /md/ (LLMs) - Returns markdown with JSON data
6
+ * 2. SSR/HTML - Pre-fetched data embedded in custom element
7
+ * 3. SPA/Browser - Custom element fetches and hydrates
8
+ */
9
+
10
+ /**
11
+ * Parsed widget block from markdown.
12
+ * Represents a fenced code block with widget syntax.
13
+ */
14
+ export interface ParsedWidgetBlock {
15
+ /** Full matched string including fences */
16
+ fullMatch: string;
17
+
18
+ /** Widget name extracted from widget:{name} */
19
+ widgetName: string;
20
+
21
+ /** Parsed JSON params, or null if empty/invalid */
22
+ params: Record<string, unknown> | null;
23
+
24
+ /** Parse error message if params JSON was invalid */
25
+ parseError?: string;
26
+
27
+ /** Start index in original markdown */
28
+ startIndex: number;
29
+
30
+ /** End index in original markdown */
31
+ endIndex: number;
32
+ }
33
+
34
+ /** Custom element tag name for widgets: `widget-{name}` */
35
+ export type WidgetTagName = `widget-${string}`;
36
+
37
+ /** SPA rendering mode. */
38
+ export type SpaMode = 'none' | 'leaf' | 'root' | 'only';
39
+
40
+ /**
41
+ * Widget manifest entry for code generation.
42
+ */
43
+ export interface WidgetManifestEntry {
44
+ /** Widget name in kebab-case */
45
+ name: string;
46
+
47
+ /** Path to widget module file */
48
+ modulePath: string;
49
+
50
+ /** Custom element tag name (widget-{name}) */
51
+ tagName: WidgetTagName;
52
+
53
+ /** Discovered/declared companion file paths (html, md, css) */
54
+ files?: { html?: string; md?: string; css?: string };
55
+ }
56
+
57
+ /**
58
+ * Generated widgets manifest structure.
59
+ */
60
+ export interface WidgetsManifest {
61
+ widgets: WidgetManifestEntry[];
62
+
63
+ /** Pre-bundled module loaders keyed by module path (for SPA bundles) */
64
+ moduleLoaders?: Record<string, () => Promise<unknown>>;
65
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Core HTML utilities for emroute
3
+ */
4
+
5
+ /** HTML attribute name marking a widget as server-rendered (skip client getData + render). */
6
+ export const SSR_ATTR = 'ssr';
7
+
8
+ /** HTML attribute name for lazy-loading widgets via IntersectionObserver. */
9
+ export const LAZY_ATTR = 'lazy';
10
+
11
+ /**
12
+ * SSR-compatible ShadowRoot mock.
13
+ * Provides a 1-to-1 subset of the browser ShadowRoot API for server-side rendering.
14
+ */
15
+ class SsrShadowRoot {
16
+ private _innerHTML = '';
17
+
18
+ constructor(public readonly host: SsrHTMLElement) {}
19
+
20
+ get innerHTML(): string {
21
+ return this._innerHTML;
22
+ }
23
+
24
+ set innerHTML(value: string) {
25
+ this._innerHTML = value;
26
+ }
27
+
28
+ setHTMLUnsafe(html: string, _options?: Record<string, unknown>): void {
29
+ this._innerHTML = html;
30
+ }
31
+
32
+ append(..._nodes: (Node | string)[]): void {
33
+ // On the server, append is a no-op — SSR content is already serialized via innerHTML.
34
+ }
35
+
36
+ querySelector(_selector: string): Element | null {
37
+ return null;
38
+ }
39
+
40
+ querySelectorAll(_selector: string): Element[] {
41
+ return [];
42
+ }
43
+
44
+ get childNodes(): Node[] {
45
+ return [];
46
+ }
47
+
48
+ get firstChild(): Node | null {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * SSR-compatible HTMLElement mock.
55
+ * Provides a 1-to-1 subset of the browser HTMLElement API for server-side rendering.
56
+ * Methods that require DOM parsing (querySelector, childNodes) return empty results —
57
+ * SSR code should use innerHTML for content, not DOM traversal.
58
+ */
59
+ class SsrHTMLElement {
60
+ private _innerHTML = '';
61
+ private _shadowRoot: SsrShadowRoot | null = null;
62
+ private _attributes = new Map<string, string>();
63
+ // Accept any CSS property assignment without error
64
+ readonly style = new Proxy({} as CSSStyleDeclaration, {
65
+ set(_target, _prop, _value) {
66
+ return true;
67
+ },
68
+ get(_target, prop) {
69
+ if (typeof prop === 'string') return '';
70
+ return undefined;
71
+ },
72
+ });
73
+
74
+ get innerHTML(): string {
75
+ return this._innerHTML;
76
+ }
77
+
78
+ set innerHTML(value: string) {
79
+ this._innerHTML = value;
80
+ }
81
+
82
+ get shadowRoot(): ShadowRoot | null {
83
+ return this._shadowRoot as unknown as ShadowRoot;
84
+ }
85
+
86
+ get childNodes(): Node[] {
87
+ return [];
88
+ }
89
+
90
+ get firstChild(): Node | null {
91
+ return null;
92
+ }
93
+
94
+ get attributes(): NamedNodeMap {
95
+ const attrs: Attr[] = [];
96
+ for (const [name, value] of this._attributes) {
97
+ attrs.push({ name, value } as Attr);
98
+ }
99
+ return attrs as unknown as NamedNodeMap;
100
+ }
101
+
102
+ attachShadow(_init: ShadowRootInit): ShadowRoot {
103
+ this._shadowRoot = new SsrShadowRoot(this);
104
+ return this._shadowRoot as unknown as ShadowRoot;
105
+ }
106
+
107
+ getAttribute(name: string): string | null {
108
+ return this._attributes.get(name) ?? null;
109
+ }
110
+
111
+ setAttribute(name: string, value: string): void {
112
+ this._attributes.set(name, value);
113
+ }
114
+
115
+ removeAttribute(name: string): void {
116
+ this._attributes.delete(name);
117
+ }
118
+
119
+ hasAttribute(name: string): boolean {
120
+ return this._attributes.has(name);
121
+ }
122
+
123
+ querySelector(_selector: string): Element | null {
124
+ return null;
125
+ }
126
+
127
+ querySelectorAll(_selector: string): Element[] {
128
+ return [];
129
+ }
130
+
131
+ append(..._nodes: (Node | string)[]): void {
132
+ // No-op on server — use innerHTML for content
133
+ }
134
+
135
+ appendChild(node: Node): Node {
136
+ return node;
137
+ }
138
+ }
139
+
140
+ /** Server-safe base class: HTMLElement in browser, SSR mock on server. */
141
+ export const HTMLElementBase = globalThis.HTMLElement ??
142
+ (SsrHTMLElement as unknown as typeof HTMLElement);
143
+
144
+ /**
145
+ * Escape HTML entities for safe display.
146
+ */
147
+ export function escapeHtml(text: string): string {
148
+ return text
149
+ .replaceAll('&', '&amp;')
150
+ .replaceAll('<', '&lt;')
151
+ .replaceAll('>', '&gt;')
152
+ .replaceAll('"', '&quot;')
153
+ .replaceAll("'", '&#39;')
154
+ .replaceAll('`', '&#96;');
155
+ }
156
+
157
+ /**
158
+ * Unescape HTML entities back to plain text (server-side, no DOM).
159
+ */
160
+ export function unescapeHtml(text: string): string {
161
+ return text
162
+ .replaceAll('&#96;', '`')
163
+ .replaceAll('&#39;', "'")
164
+ .replaceAll('&quot;', '"')
165
+ .replaceAll('&gt;', '>')
166
+ .replaceAll('&lt;', '<')
167
+ .replaceAll('&amp;', '&');
168
+ }
169
+
170
+ /**
171
+ * Wrap CSS in a `@scope` rule scoped to the widget's custom element tag.
172
+ * Used by `WidgetComponent.renderHTML()` for companion CSS files.
173
+ */
174
+ export function scopeWidgetCss(css: string, widgetName: string): string {
175
+ return `@scope (widget-${widgetName}) {\n${css}\n}`;
176
+ }
177
+
178
+ /**
179
+ * Status code to message mapping.
180
+ */
181
+ export const STATUS_MESSAGES: Record<number, string> = {
182
+ 401: 'Unauthorized',
183
+ 403: 'Forbidden',
184
+ 404: 'Not Found',
185
+ 500: 'Internal Server Error',
186
+ };
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Logger Utility
3
+ *
4
+ * Provides structured logging for emroute internals.
5
+ * Enable via localStorage: localStorage.setItem('emroute:debug', 'true')
6
+ */
7
+
8
+ const STORAGE_KEY = 'emroute:debug';
9
+ const PREFIX = '[emroute]';
10
+
11
+ function isEnabled(): boolean {
12
+ if (typeof globalThis.localStorage === 'undefined') return false;
13
+ return globalThis.localStorage.getItem(STORAGE_KEY) === 'true';
14
+ }
15
+
16
+ export const logger = {
17
+ /** Enable debug logging (persists in localStorage) */
18
+ enable(): void {
19
+ if (typeof globalThis.localStorage !== 'undefined') {
20
+ globalThis.localStorage.setItem(STORAGE_KEY, 'true');
21
+ console.log(`${PREFIX} Debug logging enabled`);
22
+ }
23
+ },
24
+
25
+ /** Disable debug logging */
26
+ disable(): void {
27
+ if (typeof globalThis.localStorage !== 'undefined') {
28
+ globalThis.localStorage.removeItem(STORAGE_KEY);
29
+ console.log(`${PREFIX} Debug logging disabled`);
30
+ }
31
+ },
32
+
33
+ /** Log general information */
34
+ info(category: string, message: string, data?: unknown): void {
35
+ if (!isEnabled()) return;
36
+ const prefix = `${PREFIX} [${category}]`;
37
+ if (data !== undefined) {
38
+ console.log(prefix, message, data);
39
+ } else {
40
+ console.log(prefix, message);
41
+ }
42
+ },
43
+
44
+ /** Log navigation events */
45
+ nav(action: string, from: string, to: string, data?: Record<string, unknown>): void {
46
+ if (!isEnabled()) return;
47
+ console.log(`${PREFIX} [nav] ${action}:`, { from, to, ...(data ?? {}) });
48
+ },
49
+
50
+ /** Log rendering events */
51
+ render(component: string, route: string, mode?: string): void {
52
+ if (!isEnabled()) return;
53
+ const modeStr = mode ? ` [mode=${mode}]` : '';
54
+ console.log(`${PREFIX} [render]${modeStr} ${component} → ${route}`);
55
+ },
56
+
57
+ /** Log link interception */
58
+ link(action: 'intercept' | 'passthrough', href: string, reason?: string): void {
59
+ if (!isEnabled()) return;
60
+ const reasonStr = reason ? ` (${reason})` : '';
61
+ console.log(`${PREFIX} [link] ${action}: ${href}${reasonStr}`);
62
+ },
63
+
64
+ /** Log widget lifecycle */
65
+ widget(event: string, name: string, data?: unknown): void {
66
+ if (!isEnabled()) return;
67
+ console.log(`${PREFIX} [widget] ${event}: ${name}`, data ?? '');
68
+ },
69
+
70
+ /** Log warnings (always shown, not gated by debug flag) */
71
+ warn(message: string): void {
72
+ console.warn(`${PREFIX}`, message);
73
+ },
74
+
75
+ /** Log SSR adoption */
76
+ ssr(action: string, route: string): void {
77
+ if (!isEnabled()) return;
78
+ console.log(`${PREFIX} [ssr] ${action}: ${route}`);
79
+ },
80
+ };
81
+
82
+ // Expose globally for console access
83
+ (globalThis as Record<string, unknown>).__emroute_logger = logger;