@withl5e/l5e 0.1.3 → 0.2.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/dist/entry-server-DJzzhCmz.js +224 -0
- package/dist/entry-server-DJzzhCmz.js.map +1 -0
- package/dist/entry-server.js +7 -6
- package/dist/entry-server.js.map +1 -1
- package/dist/exceptions-CHPz01DJ.js +39 -0
- package/dist/exceptions-CHPz01DJ.js.map +1 -0
- package/dist/{generateMetadata-C5QsMS-H.js → generateMetadata-DLMaI0EO.js} +2 -2
- package/dist/{generateMetadata-C5QsMS-H.js.map → generateMetadata-DLMaI0EO.js.map} +1 -1
- package/dist/index.js +41 -40
- package/dist/index.js.map +1 -1
- package/dist/island.js +1 -1
- package/dist/{jsx-runtime-C2Vw67N2.js → jsx-runtime-DVQ9eOji.js} +2 -1
- package/dist/{jsx-runtime-C2Vw67N2.js.map → jsx-runtime-DVQ9eOji.js.map} +1 -1
- package/dist/jsx-runtime.js +1 -1
- package/dist/router.js +72 -0
- package/dist/router.js.map +1 -0
- package/dist/seo.js +2 -2
- package/package.json +6 -1
- package/src/core/entry-server.ts +13 -2
- package/src/core/index.ts +1 -0
- package/src/core/jsx-runtime.ts +1 -0
- package/src/router/define-routes.ts +117 -0
- package/src/router/index.ts +9 -0
- package/src/router/specificity.ts +51 -0
- package/dist/entry-server-Ckh6zfgm.js +0 -258
- package/dist/entry-server-Ckh6zfgm.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,49 +1,50 @@
|
|
|
1
|
-
import { B as s, H as t, I as r, N as n, R as o, S as i
|
|
2
|
-
import { F as
|
|
3
|
-
import { coreVite as
|
|
4
|
-
import { c as
|
|
5
|
-
import { createServer as
|
|
1
|
+
import { B as s, H as t, I as r, N as n, R as o, S as i } from "./exceptions-CHPz01DJ.js";
|
|
2
|
+
import { F as d, H as l, a as p, b as x, c as m, g as E, d as g, e as u, f as S, h as C, i as f, j as H, k as R, p as h, l as I, r as v, n as T, o as b, s as q, u as F, q as M, t as N } from "./jsx-runtime-DVQ9eOji.js";
|
|
3
|
+
import { coreVite as L } from "./vite-plugin.js";
|
|
4
|
+
import { c as j, d as w, i as y, s as z, t as B } from "./index-BIt7MJT9.js";
|
|
5
|
+
import { createServer as k, startServer as A } from "./server.js";
|
|
6
|
+
import { e as O, a as U, r as Y, b as _, c as G } from "./entry-server-DJzzhCmz.js";
|
|
6
7
|
export {
|
|
7
8
|
s as BadRequestException,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
d as Fragment,
|
|
10
|
+
l as HEAD_PRIORITY,
|
|
11
|
+
p as Head,
|
|
11
12
|
t as HttpException,
|
|
12
13
|
r as InternalServerErrorException,
|
|
13
14
|
n as NotFoundException,
|
|
14
15
|
o as RedirectException,
|
|
15
16
|
i as ServiceUnavailableException,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
17
|
+
x as addCacheTag,
|
|
18
|
+
m as cloneElement,
|
|
19
|
+
L as coreVite,
|
|
20
|
+
j as createContext,
|
|
21
|
+
k as createServer,
|
|
22
|
+
w as defineMiddleware,
|
|
23
|
+
O as escapeHTML,
|
|
24
|
+
U as escapeProp,
|
|
25
|
+
E as getCacheTags,
|
|
26
|
+
g as getClientJsEntries,
|
|
27
|
+
u as getCssEntries,
|
|
28
|
+
S as getHeadContent,
|
|
29
|
+
C as getIslandEntries,
|
|
30
|
+
f as getSchemas,
|
|
31
|
+
y as isLocalsSerializable,
|
|
32
|
+
H as isValidElement,
|
|
33
|
+
R as jsxFactory,
|
|
34
|
+
h as pushMetadata,
|
|
35
|
+
I as pushSchema,
|
|
36
|
+
v as registerIsland,
|
|
37
|
+
Y as render,
|
|
38
|
+
_ as renderJsxToHtmlString,
|
|
39
|
+
G as renderToRenderedNode,
|
|
40
|
+
T as resolveMetadata,
|
|
41
|
+
b as runInRenderContext,
|
|
42
|
+
z as sequence,
|
|
43
|
+
q as setViewName,
|
|
44
|
+
A as startServer,
|
|
45
|
+
B as trySerializeLocals,
|
|
46
|
+
F as useClientJs,
|
|
47
|
+
M as useCss,
|
|
48
|
+
N as useRequest
|
|
48
49
|
};
|
|
49
50
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;"}
|
package/dist/island.js
CHANGED
|
@@ -208,6 +208,7 @@ function H() {
|
|
|
208
208
|
request: t.request,
|
|
209
209
|
view: t.viewName,
|
|
210
210
|
locals: t.request.locals ?? {},
|
|
211
|
+
params: t.request.params ?? {},
|
|
211
212
|
// Add cache tags
|
|
212
213
|
addCacheTag: (e) => {
|
|
213
214
|
p(e);
|
|
@@ -253,4 +254,4 @@ export {
|
|
|
253
254
|
H as t,
|
|
254
255
|
y as u
|
|
255
256
|
};
|
|
256
|
-
//# sourceMappingURL=jsx-runtime-
|
|
257
|
+
//# sourceMappingURL=jsx-runtime-DVQ9eOji.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jsx-runtime-C2Vw67N2.js","sources":["../src/core/head-priority.ts","../src/seo/mergeMetadata.ts","../src/core/const.ts","../src/core/jsx-runtime.ts"],"sourcesContent":["/**\n * Priority constants for Head component rendering order\n * Lower numbers render first (higher priority)\n */\nexport const HEAD_PRIORITY = {\n CRITICAL: 0, // charset, viewport - must be first\n HIGH: 10, // title, description, canonical\n MEDIUM: 50, // meta tags, robots\n SEO: 80, // OpenGraph, Twitter\n LOW: 100, // Custom head elements\n SCRIPTS: 200, // Scripts\n STYLES: 300, // Styles\n} as const;\n\nexport type HeadPriority = (typeof HEAD_PRIORITY)[keyof typeof HEAD_PRIORITY] | number;\n","import type { Metadata } from './types';\n\n/**\n * Merges parent and child metadata objects\n * Follows Next.js shallow merge pattern with deep merge for nested objects\n *\n * @param parent - Parent metadata (from layout/global loader)\n * @param child - Child metadata (from page/view loader)\n * @returns Merged metadata object\n */\nexport function mergeMetadata(\n parent: Metadata | null | undefined,\n child: Metadata | null | undefined,\n): Metadata {\n // If no parent, return child (or empty object)\n if (!parent) {\n return child || {};\n }\n\n // If no child, return parent\n if (!child) {\n return parent;\n }\n\n // Shallow merge base properties (child overrides parent)\n const merged: Metadata = {\n ...parent,\n ...child,\n };\n\n // Deep merge for nested objects\n // OpenGraph: merge nested properties\n if (child.openGraph || parent.openGraph) {\n if (child.openGraph && parent.openGraph) {\n merged.openGraph = {\n ...parent.openGraph,\n ...child.openGraph,\n // Deep merge for nested arrays/objects in openGraph\n images: child.openGraph.images ?? parent.openGraph.images,\n videos: child.openGraph.videos ?? parent.openGraph.videos,\n audio: child.openGraph.audio ?? parent.openGraph.audio,\n alternateLocale: child.openGraph.alternateLocale ?? parent.openGraph.alternateLocale,\n authors: child.openGraph.authors ?? parent.openGraph.authors,\n tags: child.openGraph.tags ?? parent.openGraph.tags,\n };\n } else {\n merged.openGraph = child.openGraph || parent.openGraph;\n }\n }\n\n // Twitter: merge nested properties\n if (child.twitter || parent.twitter) {\n if (child.twitter && parent.twitter) {\n merged.twitter = {\n ...parent.twitter,\n ...child.twitter,\n // Deep merge for nested objects\n images: child.twitter.images ?? parent.twitter.images,\n app: child.twitter.app\n ? {\n ...parent.twitter.app,\n ...child.twitter.app,\n id: {\n ...parent.twitter.app?.id,\n ...child.twitter.app.id,\n },\n url: {\n ...parent.twitter.app?.url,\n ...child.twitter.app.url,\n },\n }\n : parent.twitter.app,\n };\n } else {\n merged.twitter = child.twitter || parent.twitter;\n }\n }\n\n // Icons: merge nested properties\n if (child.icons || parent.icons) {\n if (child.icons && parent.icons) {\n merged.icons = {\n icon: child.icons.icon ?? parent.icons.icon,\n shortcut: child.icons.shortcut ?? parent.icons.shortcut,\n apple: child.icons.apple ?? parent.icons.apple,\n other: child.icons.other ?? parent.icons.other,\n };\n } else {\n merged.icons = child.icons || parent.icons;\n }\n }\n\n // Verification: merge nested properties\n if (child.verification || parent.verification) {\n if (child.verification && parent.verification) {\n merged.verification = {\n ...parent.verification,\n ...child.verification,\n // Deep merge for other verification tags\n other: child.verification.other\n ? {\n ...parent.verification.other,\n ...child.verification.other,\n }\n : parent.verification.other,\n me: child.verification.me ?? parent.verification.me,\n };\n } else {\n merged.verification = child.verification || parent.verification;\n }\n }\n\n // AppLinks: merge nested properties\n if (child.appLinks || parent.appLinks) {\n if (child.appLinks && parent.appLinks) {\n merged.appLinks = {\n ios: child.appLinks.ios\n ? {\n ...parent.appLinks.ios,\n ...child.appLinks.ios,\n }\n : parent.appLinks.ios,\n android: child.appLinks.android\n ? {\n ...parent.appLinks.android,\n ...child.appLinks.android,\n }\n : parent.appLinks.android,\n web: child.appLinks.web\n ? {\n ...parent.appLinks.web,\n ...child.appLinks.web,\n }\n : parent.appLinks.web,\n };\n } else {\n merged.appLinks = child.appLinks || parent.appLinks;\n }\n }\n\n // FormatDetection: merge nested properties\n if (child.formatDetection || parent.formatDetection) {\n if (child.formatDetection && parent.formatDetection) {\n merged.formatDetection = {\n ...parent.formatDetection,\n ...child.formatDetection,\n };\n } else {\n merged.formatDetection = child.formatDetection || parent.formatDetection;\n }\n }\n\n // Viewport: merge if both are objects\n if (child.viewport && parent.viewport) {\n if (typeof child.viewport === 'object' && typeof parent.viewport === 'object') {\n merged.viewport = {\n ...parent.viewport,\n ...child.viewport,\n } as Metadata['viewport'];\n } else {\n // If either is string, child overrides\n merged.viewport = child.viewport;\n }\n }\n\n // Robots: merge if both are objects\n if (child.robots && parent.robots) {\n if (typeof child.robots === 'object' && typeof parent.robots === 'object') {\n merged.robots = {\n ...parent.robots,\n ...child.robots,\n } as Metadata['robots'];\n } else {\n // If either is string, child overrides\n merged.robots = child.robots;\n }\n }\n\n // Array fields: child overrides parent (shallow merge behavior)\n // These are already handled by spread operator above\n // But we explicitly handle them for clarity:\n merged.keywords = child.keywords ?? parent.keywords;\n merged.themeColor = child.themeColor ?? parent.themeColor;\n merged.archives = child.archives ?? parent.archives;\n merged.assets = child.assets ?? parent.assets;\n\n // Other: merge objects\n if (child.other || parent.other) {\n if (child.other && parent.other) {\n merged.other = {\n ...parent.other,\n ...child.other,\n };\n } else {\n merged.other = child.other || parent.other;\n }\n }\n\n return merged;\n}\n","// Marker object để đánh dấu raw HTML không escape\nexport const RAW_HTML_MARKER = Symbol('rawHtml');\n","import { mergeMetadata } from '../seo/mergeMetadata';\nimport type { Metadata } from '../seo/types';\nimport { RAW_HTML_MARKER } from './const';\nimport { RequestInfo } from './entry-server';\nimport { HEAD_PRIORITY, type HeadPriority } from './head-priority';\n\nexport type JSXChild =\n | string\n | number\n | boolean\n | null\n | undefined\n | JSXNode\n | JSXChild[]\n | RawHtmlObject\n | HtmlContentObject;\n\nexport type RawHtmlObject = {\n [RAW_HTML_MARKER]: true;\n content: string;\n};\n\nexport type HtmlContentObject = {\n htmlContent: string;\n};\n\nexport type RenderedNode = {\n string: string;\n};\n\nexport type JSXNode = {\n type: string | ((props: any) => JSXChild);\n props: Record<string, any>;\n children: JSXChild[];\n};\n\nexport function jsxFactory(type: any, props: any, ...children: any): JSXNode {\n return { type, props: props || {}, children: children.flat() };\n}\n\nexport function Fragment({\n children,\n ...props\n}: { children?: JSXChild; setHtml?: unknown } & Record<string, any>): JSXChild {\n // Hỗ trợ setHtml cho Fragment\n if (props.setHtml !== undefined) {\n // Trả về object đặc biệt để không bị escape\n return { [RAW_HTML_MARKER]: true, content: props.setHtml?.toString() || '' };\n }\n return children;\n}\n\n// AsyncLocalStorage for render context (request-level)\nimport { AsyncLocalStorage } from 'async_hooks';\n\ninterface HeadEntry {\n content: JSXChild;\n priority: number; // Số càng nhỏ, render càng sớm\n source?: string; // Để debug (ví dụ: 'layout', 'page', 'seo')\n}\n\ninterface IslandEntry {\n key: string; // \"Counter_a3f2\" — registry key\n src: string; // \"src/views/.../Counter.tsx\" — manifest-compatible path\n name: string; // \"Counter\" — export name\n}\n\ninterface RenderContext {\n clientJsRegistry: Array<{ path: string; from: string }>;\n cssRegistry: Array<{ path: string; from: string }>;\n islandRegistry: IslandEntry[];\n cacheTags: Set<string>;\n headRegistry: HeadEntry[]; // Thay vì JSXChild[]\n metadataStack: Metadata[]; // Stack để track metadata hierarchy\n schemaRegistry: Array<Record<string, any>>; // Schema.org structured data từ loaders\n request: RequestInfo;\n viewName?: string; // View name from route handler\n}\n\nconst renderStore = new AsyncLocalStorage<RenderContext>();\n\n// Create context for each request\nfunction createRequestContext(requestInfo: RequestInfo): RenderContext {\n return {\n clientJsRegistry: [],\n cssRegistry: [],\n islandRegistry: [],\n cacheTags: new Set(),\n headRegistry: [],\n metadataStack: [],\n schemaRegistry: [],\n request: requestInfo,\n };\n}\n\nexport function useClientJs(path: string): string {\n if (typeof path === 'string' && path.length > 0) {\n const renderContext = renderStore.getStore();\n if (renderContext) {\n renderContext.clientJsRegistry.push({ path, from: 'Unknown' });\n }\n }\n return '';\n}\n\nexport function registerIsland(key: string, src: string, name: string): void {\n const renderContext = renderStore.getStore();\n if (renderContext) {\n // Dedupe by key\n if (!renderContext.islandRegistry.some((e) => e.key === key)) {\n renderContext.islandRegistry.push({ key, src, name });\n }\n }\n}\n\nexport function getIslandEntries(): IslandEntry[] {\n const context = renderStore.getStore();\n if (!context) return [];\n return context.islandRegistry.slice();\n}\n\nexport function useCss(path: string): string {\n if (typeof path === 'string' && path.length > 0) {\n const renderContext = renderStore.getStore();\n if (renderContext) {\n renderContext.cssRegistry.push({ path, from: 'Unknown' });\n }\n }\n return '';\n}\n\n// Wrapper to run render in async context\nexport function runInRenderContext<T>(\n renderFn: () => T | Promise<T>,\n requestInfo: RequestInfo,\n viewName?: string,\n): Promise<T> {\n const context = createRequestContext(requestInfo);\n if (viewName) {\n context.viewName = viewName;\n }\n return renderStore.run(context, () => Promise.resolve(renderFn()));\n}\n\n// Set view name in current render context\nexport function setViewName(viewName: string): void {\n const context = renderStore.getStore();\n if (context) {\n context.viewName = viewName;\n }\n}\n\n// Get entries from current context\nexport function getClientJsEntries(): Array<{ path: string; from: string }> {\n const context = renderStore.getStore();\n if (!context) return [];\n return context.clientJsRegistry.slice();\n}\n\n// Get cache tags from current context\nexport function getCacheTags(): string[] {\n const context = renderStore.getStore();\n if (!context) return [];\n return Array.from(context.cacheTags);\n}\n\n// Add cache tags to current context\nexport function addCacheTag(tag: string | string[] | Record<string, boolean>): void {\n const context = renderStore.getStore();\n if (!context) return;\n\n if (Array.isArray(tag)) {\n tag.forEach((t) => {\n if (typeof t === 'string' && t.trim()) {\n context.cacheTags.add(t.trim());\n }\n });\n } else if (typeof tag === 'string' && tag.trim()) {\n context.cacheTags.add(tag.trim());\n } else if (typeof tag === 'object' && tag !== null) {\n Object.entries(tag).forEach(([key, value]) => {\n if (value && typeof key === 'string' && key.trim()) {\n context.cacheTags.add(key.trim());\n }\n });\n }\n}\n\n// Get CSS entries from current context\nexport function getCssEntries(): Array<{ path: string; from: string }> {\n const context = renderStore.getStore();\n if (!context) return [];\n return context.cssRegistry.slice();\n}\n\n// Head component to collect head elements with priority support\nexport function Head({\n children,\n priority = HEAD_PRIORITY.LOW, // Default priority\n}: {\n children?: JSXChild;\n priority?: HeadPriority;\n}): null {\n const renderContext = renderStore.getStore();\n if (renderContext && children) {\n renderContext.headRegistry.push({\n content: children,\n priority: typeof priority === 'number' ? priority : HEAD_PRIORITY.LOW,\n source: 'manual',\n });\n\n // Sort theo priority sau mỗi lần push để đảm bảo thứ tự đúng\n renderContext.headRegistry.sort((a, b) => a.priority - b.priority);\n }\n return null;\n}\n\n// Get head content from current context (already sorted by priority)\nexport function getHeadContent(): JSXChild[] {\n const context = renderStore.getStore();\n if (!context) return [];\n // Đã được sort trong Head component, chỉ cần map để lấy content\n return context.headRegistry.map((entry) => entry.content);\n}\n\n// Push metadata to stack (for hierarchical metadata support)\nexport function pushMetadata(metadata: Metadata): void {\n const context = renderStore.getStore();\n if (context && metadata) {\n context.metadataStack.push(metadata);\n }\n}\n\n// Resolve and merge all metadata from stack (root → leaf)\nexport function resolveMetadata(): Metadata | null {\n const context = renderStore.getStore();\n if (!context || context.metadataStack.length === 0) {\n return null;\n }\n\n // Merge from root → leaf (reduce left to right)\n return context.metadataStack.reduce(\n (acc, current) => mergeMetadata(acc, current),\n null as Metadata | null,\n );\n}\n\n// Push schema to registry (for schema markup from loaders)\n// Accepts schema-dts types (WithContext<T> or array of schemas)\nexport function pushSchema(schema: any | Array<any>): void {\n const context = renderStore.getStore();\n if (!context) return;\n\n if (Array.isArray(schema)) {\n context.schemaRegistry.push(...schema);\n } else {\n context.schemaRegistry.push(schema);\n }\n}\n\n// Get all schemas from registry\nexport function getSchemas(): Array<Record<string, any>> {\n const context = renderStore.getStore();\n if (!context) return [];\n return context.schemaRegistry.slice();\n}\n\n// Hook to get render request context\nexport function useRequest() {\n const context = renderStore.getStore();\n\n if (!context) {\n throw new Error('useRequest called outside of render context');\n }\n\n return {\n request: context.request,\n view: context.viewName,\n locals: (context.request.locals ?? {}) as Record<string, unknown>,\n\n // Add cache tags\n addCacheTag: (tag: string | string[] | Record<string, boolean>) => {\n addCacheTag(tag);\n },\n\n // Get all cache tags\n getCacheTags: () => {\n return Array.from(context.cacheTags);\n },\n };\n}\n\n/**\n * Checks if a value is a valid JSX element (JSXNode)\n * Similar to React.isValidElement\n */\nexport function isValidElement(value: any): value is JSXNode {\n return (\n value !== null &&\n typeof value === 'object' &&\n 'type' in value &&\n 'props' in value &&\n 'children' in value &&\n (typeof value.type === 'string' || typeof value.type === 'function')\n );\n}\n\n/**\n * Clones a JSX element with new props and/or children\n * Similar to React.cloneElement\n */\nexport function cloneElement(\n element: JSXNode,\n props?: Record<string, any>,\n ...children: JSXChild[]\n): JSXNode {\n const newProps = { ...element.props, ...props };\n const newChildren = children.length > 0 ? children.flat() : element.children;\n\n return {\n type: element.type,\n props: newProps,\n children: newChildren,\n };\n}\n"],"names":["HEAD_PRIORITY","mergeMetadata","parent","child","merged","RAW_HTML_MARKER","jsxFactory","type","props","children","Fragment","renderStore","AsyncLocalStorage","createRequestContext","requestInfo","useClientJs","path","renderContext","registerIsland","key","src","name","e","getIslandEntries","context","useCss","runInRenderContext","renderFn","viewName","setViewName","getClientJsEntries","getCacheTags","addCacheTag","tag","t","value","getCssEntries","Head","priority","a","b","getHeadContent","entry","pushMetadata","metadata","resolveMetadata","acc","current","pushSchema","schema","getSchemas","useRequest","isValidElement","cloneElement","element","newProps","newChildren"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"jsx-runtime-DVQ9eOji.js","sources":["../src/core/head-priority.ts","../src/seo/mergeMetadata.ts","../src/core/const.ts","../src/core/jsx-runtime.ts"],"sourcesContent":["/**\n * Priority constants for Head component rendering order\n * Lower numbers render first (higher priority)\n */\nexport const HEAD_PRIORITY = {\n CRITICAL: 0, // charset, viewport - must be first\n HIGH: 10, // title, description, canonical\n MEDIUM: 50, // meta tags, robots\n SEO: 80, // OpenGraph, Twitter\n LOW: 100, // Custom head elements\n SCRIPTS: 200, // Scripts\n STYLES: 300, // Styles\n} as const;\n\nexport type HeadPriority = (typeof HEAD_PRIORITY)[keyof typeof HEAD_PRIORITY] | number;\n","import type { Metadata } from './types';\n\n/**\n * Merges parent and child metadata objects\n * Follows Next.js shallow merge pattern with deep merge for nested objects\n *\n * @param parent - Parent metadata (from layout/global loader)\n * @param child - Child metadata (from page/view loader)\n * @returns Merged metadata object\n */\nexport function mergeMetadata(\n parent: Metadata | null | undefined,\n child: Metadata | null | undefined,\n): Metadata {\n // If no parent, return child (or empty object)\n if (!parent) {\n return child || {};\n }\n\n // If no child, return parent\n if (!child) {\n return parent;\n }\n\n // Shallow merge base properties (child overrides parent)\n const merged: Metadata = {\n ...parent,\n ...child,\n };\n\n // Deep merge for nested objects\n // OpenGraph: merge nested properties\n if (child.openGraph || parent.openGraph) {\n if (child.openGraph && parent.openGraph) {\n merged.openGraph = {\n ...parent.openGraph,\n ...child.openGraph,\n // Deep merge for nested arrays/objects in openGraph\n images: child.openGraph.images ?? parent.openGraph.images,\n videos: child.openGraph.videos ?? parent.openGraph.videos,\n audio: child.openGraph.audio ?? parent.openGraph.audio,\n alternateLocale: child.openGraph.alternateLocale ?? parent.openGraph.alternateLocale,\n authors: child.openGraph.authors ?? parent.openGraph.authors,\n tags: child.openGraph.tags ?? parent.openGraph.tags,\n };\n } else {\n merged.openGraph = child.openGraph || parent.openGraph;\n }\n }\n\n // Twitter: merge nested properties\n if (child.twitter || parent.twitter) {\n if (child.twitter && parent.twitter) {\n merged.twitter = {\n ...parent.twitter,\n ...child.twitter,\n // Deep merge for nested objects\n images: child.twitter.images ?? parent.twitter.images,\n app: child.twitter.app\n ? {\n ...parent.twitter.app,\n ...child.twitter.app,\n id: {\n ...parent.twitter.app?.id,\n ...child.twitter.app.id,\n },\n url: {\n ...parent.twitter.app?.url,\n ...child.twitter.app.url,\n },\n }\n : parent.twitter.app,\n };\n } else {\n merged.twitter = child.twitter || parent.twitter;\n }\n }\n\n // Icons: merge nested properties\n if (child.icons || parent.icons) {\n if (child.icons && parent.icons) {\n merged.icons = {\n icon: child.icons.icon ?? parent.icons.icon,\n shortcut: child.icons.shortcut ?? parent.icons.shortcut,\n apple: child.icons.apple ?? parent.icons.apple,\n other: child.icons.other ?? parent.icons.other,\n };\n } else {\n merged.icons = child.icons || parent.icons;\n }\n }\n\n // Verification: merge nested properties\n if (child.verification || parent.verification) {\n if (child.verification && parent.verification) {\n merged.verification = {\n ...parent.verification,\n ...child.verification,\n // Deep merge for other verification tags\n other: child.verification.other\n ? {\n ...parent.verification.other,\n ...child.verification.other,\n }\n : parent.verification.other,\n me: child.verification.me ?? parent.verification.me,\n };\n } else {\n merged.verification = child.verification || parent.verification;\n }\n }\n\n // AppLinks: merge nested properties\n if (child.appLinks || parent.appLinks) {\n if (child.appLinks && parent.appLinks) {\n merged.appLinks = {\n ios: child.appLinks.ios\n ? {\n ...parent.appLinks.ios,\n ...child.appLinks.ios,\n }\n : parent.appLinks.ios,\n android: child.appLinks.android\n ? {\n ...parent.appLinks.android,\n ...child.appLinks.android,\n }\n : parent.appLinks.android,\n web: child.appLinks.web\n ? {\n ...parent.appLinks.web,\n ...child.appLinks.web,\n }\n : parent.appLinks.web,\n };\n } else {\n merged.appLinks = child.appLinks || parent.appLinks;\n }\n }\n\n // FormatDetection: merge nested properties\n if (child.formatDetection || parent.formatDetection) {\n if (child.formatDetection && parent.formatDetection) {\n merged.formatDetection = {\n ...parent.formatDetection,\n ...child.formatDetection,\n };\n } else {\n merged.formatDetection = child.formatDetection || parent.formatDetection;\n }\n }\n\n // Viewport: merge if both are objects\n if (child.viewport && parent.viewport) {\n if (typeof child.viewport === 'object' && typeof parent.viewport === 'object') {\n merged.viewport = {\n ...parent.viewport,\n ...child.viewport,\n } as Metadata['viewport'];\n } else {\n // If either is string, child overrides\n merged.viewport = child.viewport;\n }\n }\n\n // Robots: merge if both are objects\n if (child.robots && parent.robots) {\n if (typeof child.robots === 'object' && typeof parent.robots === 'object') {\n merged.robots = {\n ...parent.robots,\n ...child.robots,\n } as Metadata['robots'];\n } else {\n // If either is string, child overrides\n merged.robots = child.robots;\n }\n }\n\n // Array fields: child overrides parent (shallow merge behavior)\n // These are already handled by spread operator above\n // But we explicitly handle them for clarity:\n merged.keywords = child.keywords ?? parent.keywords;\n merged.themeColor = child.themeColor ?? parent.themeColor;\n merged.archives = child.archives ?? parent.archives;\n merged.assets = child.assets ?? parent.assets;\n\n // Other: merge objects\n if (child.other || parent.other) {\n if (child.other && parent.other) {\n merged.other = {\n ...parent.other,\n ...child.other,\n };\n } else {\n merged.other = child.other || parent.other;\n }\n }\n\n return merged;\n}\n","// Marker object để đánh dấu raw HTML không escape\nexport const RAW_HTML_MARKER = Symbol('rawHtml');\n","import { mergeMetadata } from '../seo/mergeMetadata';\nimport type { Metadata } from '../seo/types';\nimport { RAW_HTML_MARKER } from './const';\nimport { RequestInfo } from './entry-server';\nimport { HEAD_PRIORITY, type HeadPriority } from './head-priority';\n\nexport type JSXChild =\n | string\n | number\n | boolean\n | null\n | undefined\n | JSXNode\n | JSXChild[]\n | RawHtmlObject\n | HtmlContentObject;\n\nexport type RawHtmlObject = {\n [RAW_HTML_MARKER]: true;\n content: string;\n};\n\nexport type HtmlContentObject = {\n htmlContent: string;\n};\n\nexport type RenderedNode = {\n string: string;\n};\n\nexport type JSXNode = {\n type: string | ((props: any) => JSXChild);\n props: Record<string, any>;\n children: JSXChild[];\n};\n\nexport function jsxFactory(type: any, props: any, ...children: any): JSXNode {\n return { type, props: props || {}, children: children.flat() };\n}\n\nexport function Fragment({\n children,\n ...props\n}: { children?: JSXChild; setHtml?: unknown } & Record<string, any>): JSXChild {\n // Hỗ trợ setHtml cho Fragment\n if (props.setHtml !== undefined) {\n // Trả về object đặc biệt để không bị escape\n return { [RAW_HTML_MARKER]: true, content: props.setHtml?.toString() || '' };\n }\n return children;\n}\n\n// AsyncLocalStorage for render context (request-level)\nimport { AsyncLocalStorage } from 'async_hooks';\n\ninterface HeadEntry {\n content: JSXChild;\n priority: number; // Số càng nhỏ, render càng sớm\n source?: string; // Để debug (ví dụ: 'layout', 'page', 'seo')\n}\n\ninterface IslandEntry {\n key: string; // \"Counter_a3f2\" — registry key\n src: string; // \"src/views/.../Counter.tsx\" — manifest-compatible path\n name: string; // \"Counter\" — export name\n}\n\ninterface RenderContext {\n clientJsRegistry: Array<{ path: string; from: string }>;\n cssRegistry: Array<{ path: string; from: string }>;\n islandRegistry: IslandEntry[];\n cacheTags: Set<string>;\n headRegistry: HeadEntry[]; // Thay vì JSXChild[]\n metadataStack: Metadata[]; // Stack để track metadata hierarchy\n schemaRegistry: Array<Record<string, any>>; // Schema.org structured data từ loaders\n request: RequestInfo;\n viewName?: string; // View name from route handler\n}\n\nconst renderStore = new AsyncLocalStorage<RenderContext>();\n\n// Create context for each request\nfunction createRequestContext(requestInfo: RequestInfo): RenderContext {\n return {\n clientJsRegistry: [],\n cssRegistry: [],\n islandRegistry: [],\n cacheTags: new Set(),\n headRegistry: [],\n metadataStack: [],\n schemaRegistry: [],\n request: requestInfo,\n };\n}\n\nexport function useClientJs(path: string): string {\n if (typeof path === 'string' && path.length > 0) {\n const renderContext = renderStore.getStore();\n if (renderContext) {\n renderContext.clientJsRegistry.push({ path, from: 'Unknown' });\n }\n }\n return '';\n}\n\nexport function registerIsland(key: string, src: string, name: string): void {\n const renderContext = renderStore.getStore();\n if (renderContext) {\n // Dedupe by key\n if (!renderContext.islandRegistry.some((e) => e.key === key)) {\n renderContext.islandRegistry.push({ key, src, name });\n }\n }\n}\n\nexport function getIslandEntries(): IslandEntry[] {\n const context = renderStore.getStore();\n if (!context) return [];\n return context.islandRegistry.slice();\n}\n\nexport function useCss(path: string): string {\n if (typeof path === 'string' && path.length > 0) {\n const renderContext = renderStore.getStore();\n if (renderContext) {\n renderContext.cssRegistry.push({ path, from: 'Unknown' });\n }\n }\n return '';\n}\n\n// Wrapper to run render in async context\nexport function runInRenderContext<T>(\n renderFn: () => T | Promise<T>,\n requestInfo: RequestInfo,\n viewName?: string,\n): Promise<T> {\n const context = createRequestContext(requestInfo);\n if (viewName) {\n context.viewName = viewName;\n }\n return renderStore.run(context, () => Promise.resolve(renderFn()));\n}\n\n// Set view name in current render context\nexport function setViewName(viewName: string): void {\n const context = renderStore.getStore();\n if (context) {\n context.viewName = viewName;\n }\n}\n\n// Get entries from current context\nexport function getClientJsEntries(): Array<{ path: string; from: string }> {\n const context = renderStore.getStore();\n if (!context) return [];\n return context.clientJsRegistry.slice();\n}\n\n// Get cache tags from current context\nexport function getCacheTags(): string[] {\n const context = renderStore.getStore();\n if (!context) return [];\n return Array.from(context.cacheTags);\n}\n\n// Add cache tags to current context\nexport function addCacheTag(tag: string | string[] | Record<string, boolean>): void {\n const context = renderStore.getStore();\n if (!context) return;\n\n if (Array.isArray(tag)) {\n tag.forEach((t) => {\n if (typeof t === 'string' && t.trim()) {\n context.cacheTags.add(t.trim());\n }\n });\n } else if (typeof tag === 'string' && tag.trim()) {\n context.cacheTags.add(tag.trim());\n } else if (typeof tag === 'object' && tag !== null) {\n Object.entries(tag).forEach(([key, value]) => {\n if (value && typeof key === 'string' && key.trim()) {\n context.cacheTags.add(key.trim());\n }\n });\n }\n}\n\n// Get CSS entries from current context\nexport function getCssEntries(): Array<{ path: string; from: string }> {\n const context = renderStore.getStore();\n if (!context) return [];\n return context.cssRegistry.slice();\n}\n\n// Head component to collect head elements with priority support\nexport function Head({\n children,\n priority = HEAD_PRIORITY.LOW, // Default priority\n}: {\n children?: JSXChild;\n priority?: HeadPriority;\n}): null {\n const renderContext = renderStore.getStore();\n if (renderContext && children) {\n renderContext.headRegistry.push({\n content: children,\n priority: typeof priority === 'number' ? priority : HEAD_PRIORITY.LOW,\n source: 'manual',\n });\n\n // Sort theo priority sau mỗi lần push để đảm bảo thứ tự đúng\n renderContext.headRegistry.sort((a, b) => a.priority - b.priority);\n }\n return null;\n}\n\n// Get head content from current context (already sorted by priority)\nexport function getHeadContent(): JSXChild[] {\n const context = renderStore.getStore();\n if (!context) return [];\n // Đã được sort trong Head component, chỉ cần map để lấy content\n return context.headRegistry.map((entry) => entry.content);\n}\n\n// Push metadata to stack (for hierarchical metadata support)\nexport function pushMetadata(metadata: Metadata): void {\n const context = renderStore.getStore();\n if (context && metadata) {\n context.metadataStack.push(metadata);\n }\n}\n\n// Resolve and merge all metadata from stack (root → leaf)\nexport function resolveMetadata(): Metadata | null {\n const context = renderStore.getStore();\n if (!context || context.metadataStack.length === 0) {\n return null;\n }\n\n // Merge from root → leaf (reduce left to right)\n return context.metadataStack.reduce(\n (acc, current) => mergeMetadata(acc, current),\n null as Metadata | null,\n );\n}\n\n// Push schema to registry (for schema markup from loaders)\n// Accepts schema-dts types (WithContext<T> or array of schemas)\nexport function pushSchema(schema: any | Array<any>): void {\n const context = renderStore.getStore();\n if (!context) return;\n\n if (Array.isArray(schema)) {\n context.schemaRegistry.push(...schema);\n } else {\n context.schemaRegistry.push(schema);\n }\n}\n\n// Get all schemas from registry\nexport function getSchemas(): Array<Record<string, any>> {\n const context = renderStore.getStore();\n if (!context) return [];\n return context.schemaRegistry.slice();\n}\n\n// Hook to get render request context\nexport function useRequest() {\n const context = renderStore.getStore();\n\n if (!context) {\n throw new Error('useRequest called outside of render context');\n }\n\n return {\n request: context.request,\n view: context.viewName,\n locals: (context.request.locals ?? {}) as Record<string, unknown>,\n params: (context.request.params ?? {}) as Record<string, any>,\n\n // Add cache tags\n addCacheTag: (tag: string | string[] | Record<string, boolean>) => {\n addCacheTag(tag);\n },\n\n // Get all cache tags\n getCacheTags: () => {\n return Array.from(context.cacheTags);\n },\n };\n}\n\n/**\n * Checks if a value is a valid JSX element (JSXNode)\n * Similar to React.isValidElement\n */\nexport function isValidElement(value: any): value is JSXNode {\n return (\n value !== null &&\n typeof value === 'object' &&\n 'type' in value &&\n 'props' in value &&\n 'children' in value &&\n (typeof value.type === 'string' || typeof value.type === 'function')\n );\n}\n\n/**\n * Clones a JSX element with new props and/or children\n * Similar to React.cloneElement\n */\nexport function cloneElement(\n element: JSXNode,\n props?: Record<string, any>,\n ...children: JSXChild[]\n): JSXNode {\n const newProps = { ...element.props, ...props };\n const newChildren = children.length > 0 ? children.flat() : element.children;\n\n return {\n type: element.type,\n props: newProps,\n children: newChildren,\n };\n}\n"],"names":["HEAD_PRIORITY","mergeMetadata","parent","child","merged","RAW_HTML_MARKER","jsxFactory","type","props","children","Fragment","renderStore","AsyncLocalStorage","createRequestContext","requestInfo","useClientJs","path","renderContext","registerIsland","key","src","name","e","getIslandEntries","context","useCss","runInRenderContext","renderFn","viewName","setViewName","getClientJsEntries","getCacheTags","addCacheTag","tag","t","value","getCssEntries","Head","priority","a","b","getHeadContent","entry","pushMetadata","metadata","resolveMetadata","acc","current","pushSchema","schema","getSchemas","useRequest","isValidElement","cloneElement","element","newProps","newChildren"],"mappings":"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"}
|
package/dist/jsx-runtime.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { F as t, a as n, b as r, c as d, g, d as i, e as l, f as o, h as m, i as u, j as C, k as c, p as h, l as p, r as E, n as x, o as F, s as I, u as f, q as j, t as q } from "./jsx-runtime-
|
|
1
|
+
import { F as t, a as n, b as r, c as d, g, d as i, e as l, f as o, h as m, i as u, j as C, k as c, p as h, l as p, r as E, n as x, o as F, s as I, u as f, q as j, t as q } from "./jsx-runtime-DVQ9eOji.js";
|
|
2
2
|
import "async_hooks";
|
|
3
3
|
export {
|
|
4
4
|
t as Fragment,
|
package/dist/router.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { parse as u, match as h } from "path-to-regexp";
|
|
2
|
+
import { B as m } from "./exceptions-CHPz01DJ.js";
|
|
3
|
+
function d(t) {
|
|
4
|
+
const o = u(t);
|
|
5
|
+
let e = 0, r = 0, a = 0, i = 0, p = 0;
|
|
6
|
+
for (const n of o.tokens)
|
|
7
|
+
if (n.type === "text") {
|
|
8
|
+
const s = n.value.split("/").filter((f) => f.length > 0);
|
|
9
|
+
a += s.length, p += s.length;
|
|
10
|
+
} else n.type === "param" ? (i++, p++) : n.type === "wildcard" ? (e = -1, p++) : n.type === "group" && (r = -1);
|
|
11
|
+
return Object.freeze([e, r, p, a, -i]);
|
|
12
|
+
}
|
|
13
|
+
function g(t, o) {
|
|
14
|
+
for (let e = 0; e < t.length; e++)
|
|
15
|
+
if (t[e] !== o[e]) return o[e] - t[e];
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
function R(t) {
|
|
19
|
+
if (!Array.isArray(t))
|
|
20
|
+
throw new TypeError("defineRoutes expects an array of route entries");
|
|
21
|
+
const o = t.map((e) => {
|
|
22
|
+
if (!e || typeof e.path != "string")
|
|
23
|
+
throw new TypeError(
|
|
24
|
+
`defineRoutes: each route must have a string "path" (got ${JSON.stringify(e)})`
|
|
25
|
+
);
|
|
26
|
+
if (!e.view && !e.resolve)
|
|
27
|
+
throw new TypeError(
|
|
28
|
+
`defineRoutes: route "${e.path}" must define either "view" or "resolve"`
|
|
29
|
+
);
|
|
30
|
+
let r;
|
|
31
|
+
try {
|
|
32
|
+
r = h(e.path);
|
|
33
|
+
} catch (a) {
|
|
34
|
+
throw new TypeError(
|
|
35
|
+
`defineRoutes: invalid path "${e.path}": ${a instanceof Error ? a.message : String(a)}`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return { entry: e, matchFn: r, specificity: d(e.path) };
|
|
39
|
+
});
|
|
40
|
+
return o.sort((e, r) => g(e.specificity, r.specificity)), async function(r) {
|
|
41
|
+
const a = r.pathname ?? "/";
|
|
42
|
+
for (const { entry: i, matchFn: p } of o) {
|
|
43
|
+
const n = p(a);
|
|
44
|
+
if (!n) continue;
|
|
45
|
+
let s = n.params;
|
|
46
|
+
const f = i.params?.parse ?? i.params?.schema?.parse.bind(i.params.schema);
|
|
47
|
+
if (f)
|
|
48
|
+
try {
|
|
49
|
+
s = f(s);
|
|
50
|
+
} catch (c) {
|
|
51
|
+
const l = c instanceof Error ? c.message : String(c);
|
|
52
|
+
throw new m(`Invalid route params: ${l}`, {
|
|
53
|
+
path: i.path,
|
|
54
|
+
pathname: a
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (i.resolve) {
|
|
58
|
+
const c = await i.resolve({ params: s, requestInfo: r });
|
|
59
|
+
return v(c);
|
|
60
|
+
}
|
|
61
|
+
return { view: i.view, params: s };
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function v(t) {
|
|
67
|
+
return t == null ? null : typeof t == "string" ? { view: t, params: {} } : t;
|
|
68
|
+
}
|
|
69
|
+
export {
|
|
70
|
+
R as defineRoutes
|
|
71
|
+
};
|
|
72
|
+
//# sourceMappingURL=router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.js","sources":["../src/router/specificity.ts","../src/router/define-routes.ts"],"sourcesContent":["import { parse, type Token } from 'path-to-regexp';\n\nexport type Specificity = readonly number[];\n\n/**\n * Derives a sort tuple from a route path so `defineRoutes()` can order routes\n * deterministically: more specific patterns win regardless of declaration order.\n *\n * Tuple (compared descending — higher wins):\n * [splatPenalty, groupPenalty, requiredSegments, staticSegments, -paramCount]\n *\n * splatPenalty: -1 if the path contains a wildcard (splat), else 0 → splat routes last\n * groupPenalty: -1 if the path contains an optional group, else 0 → required-only beats group-bearing at equal URL\n * requiredSegments: count of `/`-separated pieces at the top level (static text + params + wildcards)\n * staticSegments: count of those pieces that are pure static text\n * paramCount: count of `:param` tokens at the top level (negated so fewer dynamics wins the tiebreak)\n */\nexport function pathSpecificity(path: string): Specificity {\n const td = parse(path);\n let splatPenalty = 0;\n let groupPenalty = 0;\n let staticSegments = 0;\n let paramCount = 0;\n let requiredSegments = 0;\n\n for (const tok of td.tokens) {\n if (tok.type === 'text') {\n const pieces = tok.value.split('/').filter((p) => p.length > 0);\n staticSegments += pieces.length;\n requiredSegments += pieces.length;\n } else if (tok.type === 'param') {\n paramCount++;\n requiredSegments++;\n } else if (tok.type === 'wildcard') {\n splatPenalty = -1;\n requiredSegments++;\n } else if (tok.type === 'group') {\n groupPenalty = -1;\n // Group contents do not count toward requiredSegments — they're optional.\n }\n }\n\n return Object.freeze([splatPenalty, groupPenalty, requiredSegments, staticSegments, -paramCount]);\n}\n\nexport function compareSpecificity(a: Specificity, b: Specificity): number {\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return b[i] - a[i];\n }\n return 0;\n}\n","import { match as p2rMatch, type MatchFunction } from 'path-to-regexp';\nimport { BadRequestException } from '../core/exceptions';\nimport type { RequestInfo } from '../core/entry-server';\nimport { compareSpecificity, pathSpecificity, type Specificity } from './specificity';\n\nexport type RouteHandlerResult =\n | string\n | null\n | { view: string; params?: Record<string, any> };\n\nexport type ResolveContext<TParams = Record<string, any>> = {\n params: TParams;\n requestInfo: RequestInfo;\n};\n\nexport type RouteResolveResult =\n | string\n | null\n | { view: string; params?: Record<string, any> };\n\nexport type ParamsSchema<TParams = Record<string, any>> = {\n parse: (raw: Record<string, string | string[]>) => TParams;\n};\n\nexport type RouteParamsConfig = {\n parse?: (raw: Record<string, string | string[]>) => Record<string, any>;\n schema?: ParamsSchema;\n};\n\nexport type RouteEntry = {\n path: string;\n view?: string;\n params?: RouteParamsConfig;\n resolve?: (\n ctx: ResolveContext,\n ) => RouteResolveResult | Promise<RouteResolveResult>;\n};\n\ntype CompiledRoute = {\n entry: RouteEntry;\n matchFn: MatchFunction<Record<string, string | string[]>>;\n specificity: Specificity;\n};\n\nexport function defineRoutes(\n routes: RouteEntry[],\n): (requestInfo: RequestInfo) => Promise<RouteHandlerResult> {\n if (!Array.isArray(routes)) {\n throw new TypeError('defineRoutes expects an array of route entries');\n }\n\n const compiled: CompiledRoute[] = routes.map((entry) => {\n if (!entry || typeof entry.path !== 'string') {\n throw new TypeError(\n `defineRoutes: each route must have a string \"path\" (got ${JSON.stringify(entry)})`,\n );\n }\n if (!entry.view && !entry.resolve) {\n throw new TypeError(\n `defineRoutes: route \"${entry.path}\" must define either \"view\" or \"resolve\"`,\n );\n }\n let matchFn: MatchFunction<Record<string, string | string[]>>;\n try {\n matchFn = p2rMatch<Record<string, string | string[]>>(entry.path);\n } catch (err) {\n throw new TypeError(\n `defineRoutes: invalid path \"${entry.path}\": ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n return { entry, matchFn, specificity: pathSpecificity(entry.path) };\n });\n\n compiled.sort((a, b) => compareSpecificity(a.specificity, b.specificity));\n\n return async function routeHandler(\n requestInfo: RequestInfo,\n ): Promise<RouteHandlerResult> {\n const pathname = requestInfo.pathname ?? '/';\n\n for (const { entry, matchFn } of compiled) {\n const result = matchFn(pathname);\n if (!result) continue;\n\n let params: Record<string, any> = result.params;\n // `parse` wins over `schema` when both are set (most explicit escape hatch).\n const validator =\n entry.params?.parse ?? entry.params?.schema?.parse.bind(entry.params.schema);\n if (validator) {\n try {\n params = validator(params as Record<string, string | string[]>);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new BadRequestException(`Invalid route params: ${message}`, {\n path: entry.path,\n pathname,\n });\n }\n }\n\n if (entry.resolve) {\n const resolved = await entry.resolve({ params, requestInfo });\n return normalizeResolveResult(resolved);\n }\n\n return { view: entry.view!, params };\n }\n\n return null;\n };\n}\n\nfunction normalizeResolveResult(result: RouteResolveResult): RouteHandlerResult {\n if (result === null || result === undefined) return null;\n if (typeof result === 'string') return { view: result, params: {} };\n return result;\n}\n"],"names":["pathSpecificity","path","td","parse","splatPenalty","groupPenalty","staticSegments","paramCount","requiredSegments","tok","pieces","p","compareSpecificity","a","b","i","defineRoutes","routes","compiled","entry","matchFn","p2rMatch","err","requestInfo","pathname","result","params","validator","message","BadRequestException","resolved","normalizeResolveResult"],"mappings":"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"}
|
package/dist/seo.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { M as r } from "./generateMetadata-
|
|
2
|
-
import { m } from "./jsx-runtime-
|
|
1
|
+
import { M as r } from "./generateMetadata-DLMaI0EO.js";
|
|
2
|
+
import { m } from "./jsx-runtime-DVQ9eOji.js";
|
|
3
3
|
export {
|
|
4
4
|
r as MetadataRenderer,
|
|
5
5
|
m as mergeMetadata
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@withl5e/l5e",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "HTML-first SSR MPA framework with loaders, middleware, islands, actions, swap, SEO and cache controls.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -74,6 +74,10 @@
|
|
|
74
74
|
"./island/runtime": {
|
|
75
75
|
"types": "./src/island/runtime.ts",
|
|
76
76
|
"default": "./dist/island/runtime.js"
|
|
77
|
+
},
|
|
78
|
+
"./router": {
|
|
79
|
+
"types": "./src/router/index.ts",
|
|
80
|
+
"default": "./dist/router.js"
|
|
77
81
|
}
|
|
78
82
|
},
|
|
79
83
|
"dependencies": {
|
|
@@ -81,6 +85,7 @@
|
|
|
81
85
|
"compression": "^1.7.4",
|
|
82
86
|
"esbuild": "^0.24.2",
|
|
83
87
|
"express": "^4.18.2",
|
|
88
|
+
"path-to-regexp": "^8.4.2",
|
|
84
89
|
"request-ip": "^3.3.0",
|
|
85
90
|
"rollup": "^4.0.0",
|
|
86
91
|
"sirv": "^2.0.3"
|
package/src/core/entry-server.ts
CHANGED
|
@@ -68,8 +68,14 @@ export interface RequestInfo {
|
|
|
68
68
|
query?: Record<string, any>;
|
|
69
69
|
ip?: string;
|
|
70
70
|
locals?: Record<string, unknown>;
|
|
71
|
+
params?: Record<string, any>;
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
export type RouteResult =
|
|
75
|
+
| string
|
|
76
|
+
| null
|
|
77
|
+
| { view: string; params?: Record<string, any> };
|
|
78
|
+
|
|
73
79
|
// SchemaMarkup type - có thể là single schema hoặc array of schemas
|
|
74
80
|
// Sử dụng any để tương thích với schema-dts types từ frontend
|
|
75
81
|
export type SchemaMarkup = any | Array<any>;
|
|
@@ -154,9 +160,9 @@ export async function render(url: string, requestInfo: RequestInfo = {}): Promis
|
|
|
154
160
|
return runInRenderContext(async () => {
|
|
155
161
|
try {
|
|
156
162
|
// Step 1: Call route handler to get view name
|
|
157
|
-
const
|
|
163
|
+
const rawRouteResult: RouteResult = await routeHandler(requestInfo);
|
|
158
164
|
|
|
159
|
-
if (!
|
|
165
|
+
if (!rawRouteResult) {
|
|
160
166
|
// Throw NotFoundException to render error_404 view
|
|
161
167
|
throw new NotFoundException('Page not found', {
|
|
162
168
|
path: requestInfo.path,
|
|
@@ -165,6 +171,11 @@ export async function render(url: string, requestInfo: RequestInfo = {}): Promis
|
|
|
165
171
|
});
|
|
166
172
|
}
|
|
167
173
|
|
|
174
|
+
const viewName =
|
|
175
|
+
typeof rawRouteResult === 'string' ? rawRouteResult : rawRouteResult.view;
|
|
176
|
+
requestInfo.params =
|
|
177
|
+
typeof rawRouteResult === 'string' ? {} : (rawRouteResult.params ?? {});
|
|
178
|
+
|
|
168
179
|
// Set view name in render context
|
|
169
180
|
setViewName(viewName);
|
|
170
181
|
|
package/src/core/index.ts
CHANGED
package/src/core/jsx-runtime.ts
CHANGED
|
@@ -277,6 +277,7 @@ export function useRequest() {
|
|
|
277
277
|
request: context.request,
|
|
278
278
|
view: context.viewName,
|
|
279
279
|
locals: (context.request.locals ?? {}) as Record<string, unknown>,
|
|
280
|
+
params: (context.request.params ?? {}) as Record<string, any>,
|
|
280
281
|
|
|
281
282
|
// Add cache tags
|
|
282
283
|
addCacheTag: (tag: string | string[] | Record<string, boolean>) => {
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { match as p2rMatch, type MatchFunction } from 'path-to-regexp';
|
|
2
|
+
import { BadRequestException } from '../core/exceptions';
|
|
3
|
+
import type { RequestInfo } from '../core/entry-server';
|
|
4
|
+
import { compareSpecificity, pathSpecificity, type Specificity } from './specificity';
|
|
5
|
+
|
|
6
|
+
export type RouteHandlerResult =
|
|
7
|
+
| string
|
|
8
|
+
| null
|
|
9
|
+
| { view: string; params?: Record<string, any> };
|
|
10
|
+
|
|
11
|
+
export type ResolveContext<TParams = Record<string, any>> = {
|
|
12
|
+
params: TParams;
|
|
13
|
+
requestInfo: RequestInfo;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type RouteResolveResult =
|
|
17
|
+
| string
|
|
18
|
+
| null
|
|
19
|
+
| { view: string; params?: Record<string, any> };
|
|
20
|
+
|
|
21
|
+
export type ParamsSchema<TParams = Record<string, any>> = {
|
|
22
|
+
parse: (raw: Record<string, string | string[]>) => TParams;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type RouteParamsConfig = {
|
|
26
|
+
parse?: (raw: Record<string, string | string[]>) => Record<string, any>;
|
|
27
|
+
schema?: ParamsSchema;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type RouteEntry = {
|
|
31
|
+
path: string;
|
|
32
|
+
view?: string;
|
|
33
|
+
params?: RouteParamsConfig;
|
|
34
|
+
resolve?: (
|
|
35
|
+
ctx: ResolveContext,
|
|
36
|
+
) => RouteResolveResult | Promise<RouteResolveResult>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type CompiledRoute = {
|
|
40
|
+
entry: RouteEntry;
|
|
41
|
+
matchFn: MatchFunction<Record<string, string | string[]>>;
|
|
42
|
+
specificity: Specificity;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function defineRoutes(
|
|
46
|
+
routes: RouteEntry[],
|
|
47
|
+
): (requestInfo: RequestInfo) => Promise<RouteHandlerResult> {
|
|
48
|
+
if (!Array.isArray(routes)) {
|
|
49
|
+
throw new TypeError('defineRoutes expects an array of route entries');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const compiled: CompiledRoute[] = routes.map((entry) => {
|
|
53
|
+
if (!entry || typeof entry.path !== 'string') {
|
|
54
|
+
throw new TypeError(
|
|
55
|
+
`defineRoutes: each route must have a string "path" (got ${JSON.stringify(entry)})`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
if (!entry.view && !entry.resolve) {
|
|
59
|
+
throw new TypeError(
|
|
60
|
+
`defineRoutes: route "${entry.path}" must define either "view" or "resolve"`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
let matchFn: MatchFunction<Record<string, string | string[]>>;
|
|
64
|
+
try {
|
|
65
|
+
matchFn = p2rMatch<Record<string, string | string[]>>(entry.path);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
throw new TypeError(
|
|
68
|
+
`defineRoutes: invalid path "${entry.path}": ${err instanceof Error ? err.message : String(err)}`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return { entry, matchFn, specificity: pathSpecificity(entry.path) };
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
compiled.sort((a, b) => compareSpecificity(a.specificity, b.specificity));
|
|
75
|
+
|
|
76
|
+
return async function routeHandler(
|
|
77
|
+
requestInfo: RequestInfo,
|
|
78
|
+
): Promise<RouteHandlerResult> {
|
|
79
|
+
const pathname = requestInfo.pathname ?? '/';
|
|
80
|
+
|
|
81
|
+
for (const { entry, matchFn } of compiled) {
|
|
82
|
+
const result = matchFn(pathname);
|
|
83
|
+
if (!result) continue;
|
|
84
|
+
|
|
85
|
+
let params: Record<string, any> = result.params;
|
|
86
|
+
// `parse` wins over `schema` when both are set (most explicit escape hatch).
|
|
87
|
+
const validator =
|
|
88
|
+
entry.params?.parse ?? entry.params?.schema?.parse.bind(entry.params.schema);
|
|
89
|
+
if (validator) {
|
|
90
|
+
try {
|
|
91
|
+
params = validator(params as Record<string, string | string[]>);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
94
|
+
throw new BadRequestException(`Invalid route params: ${message}`, {
|
|
95
|
+
path: entry.path,
|
|
96
|
+
pathname,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (entry.resolve) {
|
|
102
|
+
const resolved = await entry.resolve({ params, requestInfo });
|
|
103
|
+
return normalizeResolveResult(resolved);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { view: entry.view!, params };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return null;
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function normalizeResolveResult(result: RouteResolveResult): RouteHandlerResult {
|
|
114
|
+
if (result === null || result === undefined) return null;
|
|
115
|
+
if (typeof result === 'string') return { view: result, params: {} };
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { parse, type Token } from 'path-to-regexp';
|
|
2
|
+
|
|
3
|
+
export type Specificity = readonly number[];
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Derives a sort tuple from a route path so `defineRoutes()` can order routes
|
|
7
|
+
* deterministically: more specific patterns win regardless of declaration order.
|
|
8
|
+
*
|
|
9
|
+
* Tuple (compared descending — higher wins):
|
|
10
|
+
* [splatPenalty, groupPenalty, requiredSegments, staticSegments, -paramCount]
|
|
11
|
+
*
|
|
12
|
+
* splatPenalty: -1 if the path contains a wildcard (splat), else 0 → splat routes last
|
|
13
|
+
* groupPenalty: -1 if the path contains an optional group, else 0 → required-only beats group-bearing at equal URL
|
|
14
|
+
* requiredSegments: count of `/`-separated pieces at the top level (static text + params + wildcards)
|
|
15
|
+
* staticSegments: count of those pieces that are pure static text
|
|
16
|
+
* paramCount: count of `:param` tokens at the top level (negated so fewer dynamics wins the tiebreak)
|
|
17
|
+
*/
|
|
18
|
+
export function pathSpecificity(path: string): Specificity {
|
|
19
|
+
const td = parse(path);
|
|
20
|
+
let splatPenalty = 0;
|
|
21
|
+
let groupPenalty = 0;
|
|
22
|
+
let staticSegments = 0;
|
|
23
|
+
let paramCount = 0;
|
|
24
|
+
let requiredSegments = 0;
|
|
25
|
+
|
|
26
|
+
for (const tok of td.tokens) {
|
|
27
|
+
if (tok.type === 'text') {
|
|
28
|
+
const pieces = tok.value.split('/').filter((p) => p.length > 0);
|
|
29
|
+
staticSegments += pieces.length;
|
|
30
|
+
requiredSegments += pieces.length;
|
|
31
|
+
} else if (tok.type === 'param') {
|
|
32
|
+
paramCount++;
|
|
33
|
+
requiredSegments++;
|
|
34
|
+
} else if (tok.type === 'wildcard') {
|
|
35
|
+
splatPenalty = -1;
|
|
36
|
+
requiredSegments++;
|
|
37
|
+
} else if (tok.type === 'group') {
|
|
38
|
+
groupPenalty = -1;
|
|
39
|
+
// Group contents do not count toward requiredSegments — they're optional.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return Object.freeze([splatPenalty, groupPenalty, requiredSegments, staticSegments, -paramCount]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function compareSpecificity(a: Specificity, b: Specificity): number {
|
|
47
|
+
for (let i = 0; i < a.length; i++) {
|
|
48
|
+
if (a[i] !== b[i]) return b[i] - a[i];
|
|
49
|
+
}
|
|
50
|
+
return 0;
|
|
51
|
+
}
|