@emberkit/core 0.3.2 → 0.3.6
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/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/signals/helpers/core.d.ts.map +1 -1
- package/dist/signals/helpers/core.js +3 -2
- package/dist/ssr/index.d.ts +1 -0
- package/dist/ssr/index.d.ts.map +1 -1
- package/dist/ssr/index.js +1 -0
- package/dist/ssr/server.d.ts +34 -0
- package/dist/ssr/server.d.ts.map +1 -0
- package/dist/ssr/server.js +148 -0
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +315 -120
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export type { Signal, WritableSignal, ReadonlySignal } from './signals/index.js'
|
|
|
11
11
|
export { createMarkdownParser, parseMarkdown, renderMarkdown, extractFrontmatter, } from './markdown/index.js';
|
|
12
12
|
export { compileMDX, compileSync, useMDX } from './mdx/index.js';
|
|
13
13
|
export { DataCache, createCache, getCached, setCache, prefetch } from './cache/index.js';
|
|
14
|
+
export { renderToHTMLString } from './ssr/helpers/render-html.js';
|
|
15
|
+
export { drainHeadContent } from './meta/head-registry.js';
|
|
14
16
|
export { Head } from './meta/index.js';
|
|
15
17
|
export type { HeadProps } from './meta/index.js';
|
|
16
18
|
export { generateMeta, generateBreadcrumbs, generateArticleSchema, generateProductSchema, } from './meta/index.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,UAAU,CAAC;AAE/B,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,EACL,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,KAAK,EACL,OAAO,EACP,MAAM,EACN,QAAQ,EACR,MAAM,GACP,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAC/D,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,KAAK,YAAY,EAAE,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAC7F,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACnF,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAC1E,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACjF,OAAO,EACL,oBAAoB,EACpB,aAAa,EACb,cAAc,EACd,kBAAkB,GACnB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAEjE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEzF,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvC,YAAY,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EACL,YAAY,EACZ,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEhF,YAAY,EAAE,EAAE,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAE5E,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAElG,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAErF"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,UAAU,CAAC;AAE/B,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,EACL,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,KAAK,EACL,OAAO,EACP,MAAM,EACN,QAAQ,EACR,MAAM,GACP,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAC/D,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,KAAK,YAAY,EAAE,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAC7F,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACnF,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAC1E,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACjF,OAAO,EACL,oBAAoB,EACpB,aAAa,EACb,cAAc,EACd,kBAAkB,GACnB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAEjE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEzF,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvC,YAAY,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EACL,YAAY,EACZ,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEhF,YAAY,EAAE,EAAE,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAE5E,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAElG,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAErF"}
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,8 @@ export { createErrorBoundary, createLoadingBoundary } from './boundaries/index.j
|
|
|
9
9
|
export { createMarkdownParser, parseMarkdown, renderMarkdown, extractFrontmatter, } from './markdown/index.js';
|
|
10
10
|
export { compileMDX, compileSync, useMDX } from './mdx/index.js';
|
|
11
11
|
export { DataCache, createCache, getCached, setCache, prefetch } from './cache/index.js';
|
|
12
|
+
export { renderToHTMLString } from './ssr/helpers/render-html.js';
|
|
13
|
+
export { drainHeadContent } from './meta/head-registry.js';
|
|
12
14
|
export { Head } from './meta/index.js';
|
|
13
15
|
export { generateMeta, generateBreadcrumbs, generateArticleSchema, generateProductSchema, } from './meta/index.js';
|
|
14
16
|
export function defineConfig(config) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../../src/signals/helpers/core.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAKzD,wBAAgB,aAAa,IAAI,IAAI,CAEpC;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,GACV;IAAE,SAAS,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,KAAK,MAAM,IAAI,CAAA;CAAE,GAAG,SAAS,CAErE;AAED,wBAAgB,YAAY,CAAC,CAAC,EAC5B,YAAY,EAAE,CAAC,EACf,OAAO,GAAE,aAAa,CAAC,CAAC,CAAM,GAC7B,CAAC,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../../src/signals/helpers/core.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAKzD,wBAAgB,aAAa,IAAI,IAAI,CAEpC;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,GACV;IAAE,SAAS,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,KAAK,MAAM,IAAI,CAAA;CAAE,GAAG,SAAS,CAErE;AAED,wBAAgB,YAAY,CAAC,CAAC,EAC5B,YAAY,EAAE,CAAC,EACf,OAAO,GAAE,aAAa,CAAC,CAAC,CAAM,GAC7B,CAAC,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAoEjE;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,EAAE,QAAQ,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAqB1F;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,MAAM,IAAI,CAqB5E;AAED,wBAAgB,KAAK,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAEvC;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAEzC;AAED,wBAAgB,MAAM,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAEpD;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAE3D;AAED,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,MAAM,IAAI,CAEtE"}
|
|
@@ -9,6 +9,7 @@ export function getSignalByIndex(idx) {
|
|
|
9
9
|
export function createSignal(initialValue, options = {}) {
|
|
10
10
|
let value = initialValue;
|
|
11
11
|
const subs = new Set();
|
|
12
|
+
const equals = options.equals ?? ((a, b) => a === b);
|
|
12
13
|
const idx = sigIndex++;
|
|
13
14
|
function getter() {
|
|
14
15
|
return value;
|
|
@@ -16,7 +17,7 @@ export function createSignal(initialValue, options = {}) {
|
|
|
16
17
|
getter.__idx = idx;
|
|
17
18
|
function setter(newValue) {
|
|
18
19
|
const next = typeof newValue === 'function' ? newValue(value) : newValue;
|
|
19
|
-
if (value
|
|
20
|
+
if (equals(value, next))
|
|
20
21
|
return;
|
|
21
22
|
value = next;
|
|
22
23
|
if (subs.size > 0) {
|
|
@@ -36,7 +37,7 @@ export function createSignal(initialValue, options = {}) {
|
|
|
36
37
|
},
|
|
37
38
|
set value(newValue) {
|
|
38
39
|
const next = typeof newValue === 'function' ? newValue(value) : newValue;
|
|
39
|
-
if (value
|
|
40
|
+
if (equals(value, next))
|
|
40
41
|
return;
|
|
41
42
|
value = next;
|
|
42
43
|
if (subs.size > 0) {
|
package/dist/ssr/index.d.ts
CHANGED
package/dist/ssr/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ssr/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,0BAA0B,CAAC;AACzC,cAAc,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ssr/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,0BAA0B,CAAC;AACzC,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC"}
|
package/dist/ssr/index.js
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { JSXNode } from '../runtime/types.js';
|
|
3
|
+
export interface SSRManifest {
|
|
4
|
+
routes: SSRRouteEntry[];
|
|
5
|
+
clientEntry: string;
|
|
6
|
+
assets: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface SSRRouteEntry {
|
|
9
|
+
path: string;
|
|
10
|
+
component: () => Promise<{
|
|
11
|
+
default: (props: Record<string, unknown>) => JSXNode;
|
|
12
|
+
}>;
|
|
13
|
+
isStatic?: boolean;
|
|
14
|
+
prerendered?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface SSRServerOptions {
|
|
17
|
+
manifest: SSRManifest;
|
|
18
|
+
mode: 'ssr' | 'hybrid' | 'static';
|
|
19
|
+
distDir: string;
|
|
20
|
+
template: string;
|
|
21
|
+
}
|
|
22
|
+
export interface SSRRequestContext {
|
|
23
|
+
url: string;
|
|
24
|
+
pathname: string;
|
|
25
|
+
params: Record<string, string>;
|
|
26
|
+
query: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
export declare function renderRoute(options: SSRServerOptions, context: SSRRequestContext): Promise<{
|
|
29
|
+
html: string;
|
|
30
|
+
status: number;
|
|
31
|
+
}>;
|
|
32
|
+
export declare function createSSRHandler(options: SSRServerOptions): (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
|
|
33
|
+
export declare function prerenderRoutes(options: SSRServerOptions, outputDir: string): Promise<Map<string, string>>;
|
|
34
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/ssr/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAInD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,OAAO,CAAC;QAAE,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAA;KAAE,CAAC,CAAC;IACnF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,WAAW,CAAC;IACtB,IAAI,EAAE,KAAK,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AA8CD,wBAAsB,WAAW,CAC/B,OAAO,EAAE,gBAAgB,EACzB,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAoD3C;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,IAC1C,KAAK,eAAe,EAAE,KAAK,cAAc,KAAG,OAAO,CAAC,OAAO,CAAC,CAuC3E;AAED,wBAAsB,eAAe,CACnC,OAAO,EAAE,gBAAgB,EACzB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAmC9B"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { renderToHTMLString } from './helpers/render-html.js';
|
|
2
|
+
import { drainHeadContent } from '../meta/head-registry.js';
|
|
3
|
+
const routeToRegex = (routePath) => {
|
|
4
|
+
const paramNames = [];
|
|
5
|
+
const regexStr = routePath
|
|
6
|
+
.replace(/:([^/]+)\*/g, (_, name) => {
|
|
7
|
+
paramNames.push(name);
|
|
8
|
+
return '(.*)';
|
|
9
|
+
})
|
|
10
|
+
.replace(/:([^/]+)/g, (_, name) => {
|
|
11
|
+
paramNames.push(name);
|
|
12
|
+
return '([^/]+)';
|
|
13
|
+
});
|
|
14
|
+
return { regex: new RegExp('^' + regexStr + '$'), paramNames };
|
|
15
|
+
};
|
|
16
|
+
const matchRoute = (routes, pathname) => {
|
|
17
|
+
const normalizedPath = pathname.replace(/\/+$/, '') || '/';
|
|
18
|
+
for (const route of routes) {
|
|
19
|
+
const pattern = routeToRegex(route.path);
|
|
20
|
+
const match = normalizedPath.match(pattern.regex);
|
|
21
|
+
if (match) {
|
|
22
|
+
const params = {};
|
|
23
|
+
pattern.paramNames.forEach((name, i) => {
|
|
24
|
+
params[name] = match[i + 1];
|
|
25
|
+
});
|
|
26
|
+
return { route, params };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
};
|
|
31
|
+
const escapeHtml = (str) => {
|
|
32
|
+
if (typeof str !== 'string')
|
|
33
|
+
return str;
|
|
34
|
+
return str
|
|
35
|
+
.replace(/&/g, '&')
|
|
36
|
+
.replace(/</g, '<')
|
|
37
|
+
.replace(/>/g, '>')
|
|
38
|
+
.replace(/"/g, '"')
|
|
39
|
+
.replace(/'/g, ''');
|
|
40
|
+
};
|
|
41
|
+
export async function renderRoute(options, context) {
|
|
42
|
+
const { manifest, template } = options;
|
|
43
|
+
const sortedRoutes = [...manifest.routes].sort((a, b) => {
|
|
44
|
+
const aScore = a.path.includes(':') ? 0 : 1;
|
|
45
|
+
const bScore = b.path.includes(':') ? 0 : 1;
|
|
46
|
+
return bScore - aScore;
|
|
47
|
+
});
|
|
48
|
+
const match = matchRoute(sortedRoutes, context.pathname);
|
|
49
|
+
let appHtml = '';
|
|
50
|
+
let headContent = '';
|
|
51
|
+
let status = 200;
|
|
52
|
+
if (match) {
|
|
53
|
+
try {
|
|
54
|
+
const mod = await match.route.component();
|
|
55
|
+
const Component = mod.default;
|
|
56
|
+
const element = Component({ params: match.params });
|
|
57
|
+
appHtml = renderToHTMLString(element);
|
|
58
|
+
const collectedHead = drainHeadContent();
|
|
59
|
+
if (collectedHead) {
|
|
60
|
+
headContent = collectedHead;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
console.error('[SSR] Failed to render route:', context.pathname, e);
|
|
65
|
+
appHtml = `<div style="color: red; padding: 20px;">SSR Error: ${escapeHtml(String(e))}</div>`;
|
|
66
|
+
status = 500;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
appHtml = '<div style="padding: 20px;">404 - Page not found</div>';
|
|
71
|
+
status = 404;
|
|
72
|
+
}
|
|
73
|
+
let html = template;
|
|
74
|
+
if (html.includes('<body id="app">')) {
|
|
75
|
+
html = html.replace('<body id="app">', '<body id="app">' + appHtml);
|
|
76
|
+
}
|
|
77
|
+
else if (html.includes('<div id="app">')) {
|
|
78
|
+
html = html.replace('<div id="app"></div>', '<div id="app">' + appHtml + '</div>');
|
|
79
|
+
}
|
|
80
|
+
else if (html.includes('<div id="app"/>')) {
|
|
81
|
+
html = html.replace('<div id="app"/>', '<div id="app">' + appHtml + '</div>');
|
|
82
|
+
}
|
|
83
|
+
if (headContent && html.includes('</head>')) {
|
|
84
|
+
html = html.replace('</head>', headContent + '</head>');
|
|
85
|
+
}
|
|
86
|
+
return { html, status };
|
|
87
|
+
}
|
|
88
|
+
export function createSSRHandler(options) {
|
|
89
|
+
return async (req, res) => {
|
|
90
|
+
const url = req.url ?? '/';
|
|
91
|
+
const urlObj = new URL(url, `http://${req.headers.host || 'localhost'}`);
|
|
92
|
+
const pathname = urlObj.pathname;
|
|
93
|
+
if (pathname.startsWith('/assets/') ||
|
|
94
|
+
pathname.includes('.') ||
|
|
95
|
+
req.headers.accept?.includes('application/json')) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
if (!req.headers.accept?.includes('text/html')) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
const context = {
|
|
102
|
+
url,
|
|
103
|
+
pathname,
|
|
104
|
+
params: {},
|
|
105
|
+
query: Object.fromEntries(urlObj.searchParams),
|
|
106
|
+
};
|
|
107
|
+
try {
|
|
108
|
+
const { html, status } = await renderRoute(options, context);
|
|
109
|
+
res.statusCode = status;
|
|
110
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
111
|
+
res.end(html);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
console.error('[SSR Server Error]', error);
|
|
116
|
+
res.statusCode = 500;
|
|
117
|
+
res.setHeader('Content-Type', 'text/html');
|
|
118
|
+
res.end('<h1>500 - Internal Server Error</h1>');
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export async function prerenderRoutes(options, outputDir) {
|
|
124
|
+
const { writeFile, mkdir } = await import('node:fs/promises');
|
|
125
|
+
const { join, dirname } = await import('node:path');
|
|
126
|
+
const prerendered = new Map();
|
|
127
|
+
const staticRoutes = options.manifest.routes.filter((route) => route.isStatic || !route.path.includes(':'));
|
|
128
|
+
for (const route of staticRoutes) {
|
|
129
|
+
const context = {
|
|
130
|
+
url: route.path,
|
|
131
|
+
pathname: route.path,
|
|
132
|
+
params: {},
|
|
133
|
+
query: {},
|
|
134
|
+
};
|
|
135
|
+
try {
|
|
136
|
+
const { html } = await renderRoute(options, context);
|
|
137
|
+
const filePath = route.path === '/' ? join(outputDir, 'index.html') : join(outputDir, route.path, 'index.html');
|
|
138
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
139
|
+
await writeFile(filePath, html, 'utf-8');
|
|
140
|
+
prerendered.set(route.path, filePath);
|
|
141
|
+
console.log(` ✓ Prerendered: ${route.path}`);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
console.error(` ✗ Failed to prerender: ${route.path}`, error);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return prerendered;
|
|
148
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAC;AAClD,OAAO,KAAK,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AA8CtE,wBAAgB,kBAAkB,CAAC,WAAW,GAAE,qBAA0B,GAAG,MAAM,CA8IlF;AA6hCD,YAAY,EAAE,qBAAqB,EAAE,YAAY,EAAE,CAAC"}
|
|
@@ -7,20 +7,47 @@ import remarkGfm from 'remark-gfm';
|
|
|
7
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
8
|
const VIRTUAL_EMBERKIT_CONFIG = 'virtual:emberkit-config';
|
|
9
9
|
const VIRTUAL_EMBERKIT_ROUTES = 'virtual:emberkit-routes';
|
|
10
|
-
|
|
10
|
+
const VIRTUAL_SSR_ENTRY = 'virtual:emberkit-ssr-entry';
|
|
11
|
+
async function loadEmberKitConfig(root) {
|
|
12
|
+
const { pathToFileURL } = await import('node:url');
|
|
13
|
+
const configPaths = [
|
|
14
|
+
join(root, 'emberkit.config.ts'),
|
|
15
|
+
join(root, 'emberkit.config.js'),
|
|
16
|
+
join(root, 'emberkit.config.mjs'),
|
|
17
|
+
];
|
|
18
|
+
for (const configPath of configPaths) {
|
|
19
|
+
if (existsSync(configPath)) {
|
|
20
|
+
try {
|
|
21
|
+
const configUrl = pathToFileURL(configPath).href;
|
|
22
|
+
const mod = await import(configUrl);
|
|
23
|
+
return mod.default || mod;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
function resolveConfig(userOptions = {}, fileConfig = {}) {
|
|
11
33
|
return {
|
|
12
34
|
...DEFAULT_CONFIG,
|
|
35
|
+
...fileConfig,
|
|
13
36
|
...userOptions,
|
|
14
|
-
markdown: { ...DEFAULT_CONFIG.markdown, ...userOptions.markdown },
|
|
37
|
+
markdown: { ...DEFAULT_CONFIG.markdown, ...fileConfig.markdown, ...userOptions.markdown },
|
|
15
38
|
};
|
|
16
39
|
}
|
|
17
40
|
export function emberkitVitePlugin(userOptions = {}) {
|
|
18
|
-
|
|
41
|
+
let options = resolveConfig(userOptions);
|
|
19
42
|
let routesCode = `export const routes = [];`;
|
|
43
|
+
let projectRoot = process.cwd();
|
|
20
44
|
return {
|
|
21
45
|
name: 'emberkit:vite-plugin',
|
|
22
46
|
enforce: 'pre',
|
|
23
|
-
async config() {
|
|
47
|
+
async config(config) {
|
|
48
|
+
projectRoot = config.root || process.cwd();
|
|
49
|
+
const fileConfig = await loadEmberKitConfig(projectRoot);
|
|
50
|
+
options = resolveConfig(userOptions, fileConfig);
|
|
24
51
|
const pkgRoot = resolve(__dirname, '..', '..');
|
|
25
52
|
const srcDir = join(pkgRoot, 'src');
|
|
26
53
|
const plugins = [];
|
|
@@ -63,6 +90,9 @@ export function emberkitVitePlugin(userOptions = {}) {
|
|
|
63
90
|
if (id === VIRTUAL_EMBERKIT_ROUTES) {
|
|
64
91
|
return VIRTUAL_EMBERKIT_ROUTES;
|
|
65
92
|
}
|
|
93
|
+
if (id === VIRTUAL_SSR_ENTRY) {
|
|
94
|
+
return VIRTUAL_SSR_ENTRY;
|
|
95
|
+
}
|
|
66
96
|
return null;
|
|
67
97
|
},
|
|
68
98
|
load(id) {
|
|
@@ -72,8 +102,42 @@ export function emberkitVitePlugin(userOptions = {}) {
|
|
|
72
102
|
if (id === VIRTUAL_EMBERKIT_ROUTES) {
|
|
73
103
|
return routesCode;
|
|
74
104
|
}
|
|
105
|
+
if (id === VIRTUAL_SSR_ENTRY) {
|
|
106
|
+
return generateSSREntry();
|
|
107
|
+
}
|
|
75
108
|
return null;
|
|
76
109
|
},
|
|
110
|
+
configureServer(server) {
|
|
111
|
+
if (options.mode === 'spa') {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
server.middlewares.use(async (req, res, next) => {
|
|
115
|
+
const url = req.url ?? '/';
|
|
116
|
+
if (url.startsWith('/@') ||
|
|
117
|
+
url.startsWith('/__') ||
|
|
118
|
+
url.startsWith('/node_modules') ||
|
|
119
|
+
url.startsWith('/src/') ||
|
|
120
|
+
url.includes('.') ||
|
|
121
|
+
req.headers.accept?.includes('application/json')) {
|
|
122
|
+
return next();
|
|
123
|
+
}
|
|
124
|
+
if (!req.headers.accept?.includes('text/html')) {
|
|
125
|
+
return next();
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const ssrModule = await server.ssrLoadModule(VIRTUAL_SSR_ENTRY);
|
|
129
|
+
const html = await ssrModule.render(url, server);
|
|
130
|
+
res.statusCode = 200;
|
|
131
|
+
res.setHeader('Content-Type', 'text/html');
|
|
132
|
+
res.end(html);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
server.ssrFixStacktrace(error);
|
|
136
|
+
console.error('[EmberKit SSR Error]', error);
|
|
137
|
+
next(error);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
},
|
|
77
141
|
transform(code, id) {
|
|
78
142
|
if (id.includes('\u0000'))
|
|
79
143
|
return null;
|
|
@@ -146,7 +210,50 @@ export default function MDComponent(props) {
|
|
|
146
210
|
`;
|
|
147
211
|
return { code: componentCode };
|
|
148
212
|
}
|
|
149
|
-
|
|
213
|
+
/** Insert a block after leading `import` lines so output stays valid ESM (imports first). */
|
|
214
|
+
function insertAfterLeadingImports(moduleSource, insertBlock) {
|
|
215
|
+
const lines = moduleSource.split('\n');
|
|
216
|
+
let i = 0;
|
|
217
|
+
while (i < lines.length) {
|
|
218
|
+
const t = lines[i].trimStart();
|
|
219
|
+
if (t === '' || t.startsWith('import ')) {
|
|
220
|
+
i++;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
const head = lines.slice(0, i).join('\n');
|
|
226
|
+
const tail = lines.slice(i).join('\n');
|
|
227
|
+
const mid = insertBlock.trim();
|
|
228
|
+
if (!mid) {
|
|
229
|
+
return moduleSource;
|
|
230
|
+
}
|
|
231
|
+
return `${head}\n\n${mid}\n\n${tail}`;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Returns true when `code` is already compiled JS (e.g. from Vite's dep
|
|
235
|
+
* pre-bundling cache) and does not need to be run through @mdx-js/mdx again.
|
|
236
|
+
*/
|
|
237
|
+
function isAlreadyCompiledMDX(code) {
|
|
238
|
+
const trimmed = code.trimStart();
|
|
239
|
+
// Compiled MDX always starts with a JS import declaration generated by
|
|
240
|
+
// @mdx-js/mdx (jsx-runtime import) or is plain JS with no MDX markers.
|
|
241
|
+
// Raw MDX starts with frontmatter (---), a heading (#), a paragraph, or
|
|
242
|
+
// a top-level JSX element (<Component...) — none of which begin with
|
|
243
|
+
// `import {` followed by jsx-runtime exports.
|
|
244
|
+
return (trimmed.startsWith('import {Fragment') ||
|
|
245
|
+
trimmed.startsWith('import{Fragment') ||
|
|
246
|
+
trimmed.startsWith('import {jsx') ||
|
|
247
|
+
trimmed.startsWith('import{jsx') ||
|
|
248
|
+
trimmed.startsWith('"use strict"'));
|
|
249
|
+
}
|
|
250
|
+
async function transformMDX(code, _id) {
|
|
251
|
+
// Guard: Vite's dep pre-bundling may pass the already-compiled JS back
|
|
252
|
+
// through the transform pipeline. Return it unchanged so we don't try to
|
|
253
|
+
// compile valid JS as MDX source.
|
|
254
|
+
if (isAlreadyCompiledMDX(code)) {
|
|
255
|
+
return { code };
|
|
256
|
+
}
|
|
150
257
|
const frontmatterMatch = code.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
151
258
|
let frontmatter = {};
|
|
152
259
|
let content = code;
|
|
@@ -155,34 +262,12 @@ async function transformMDX(code, id) {
|
|
|
155
262
|
frontmatter = parseFrontmatter(fmContent);
|
|
156
263
|
content = code.slice(frontmatterMatch[0].length);
|
|
157
264
|
}
|
|
158
|
-
|
|
159
|
-
const codeBlocks = [];
|
|
160
|
-
const processedContent = content.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang, blockCode) => {
|
|
161
|
-
const html = renderCodeBlock(lang, blockCode);
|
|
162
|
-
codeBlocks.push({ html, index: codeBlocks.length });
|
|
163
|
-
return `<CodeBlock_${codeBlocks.length - 1} />`;
|
|
164
|
-
});
|
|
165
|
-
const compiled = await compile(processedContent, {
|
|
166
|
-
outputFormat: 'program',
|
|
167
|
-
development: false,
|
|
265
|
+
const compiled = await compile(content, {
|
|
168
266
|
jsx: false,
|
|
169
267
|
jsxImportSource: '@emberkit/core',
|
|
170
268
|
remarkPlugins: [remarkGfm],
|
|
269
|
+
development: false,
|
|
171
270
|
});
|
|
172
|
-
let compiledCode = String(compiled);
|
|
173
|
-
// Build code block component definitions
|
|
174
|
-
const codeBlockComponents = codeBlocks
|
|
175
|
-
.map((block) => {
|
|
176
|
-
const escapedHtml = JSON.stringify(block.html);
|
|
177
|
-
return `function CodeBlock_${block.index}() {
|
|
178
|
-
return createElement('div', {
|
|
179
|
-
dangerouslySetInnerHTML: { __html: ${escapedHtml} }
|
|
180
|
-
});
|
|
181
|
-
}`;
|
|
182
|
-
})
|
|
183
|
-
.join('\n\n');
|
|
184
|
-
// Rename the MDX default export so we can wrap it
|
|
185
|
-
compiledCode = compiledCode.replace('export default function MDXContent', 'function _MDXContent');
|
|
186
271
|
const exportLines = [];
|
|
187
272
|
if (frontmatter.title) {
|
|
188
273
|
exportLines.push(`export const title = ${JSON.stringify(frontmatter.title)};`);
|
|
@@ -197,70 +282,27 @@ async function transformMDX(code, id) {
|
|
|
197
282
|
exportLines.push(`export const date = ${JSON.stringify(frontmatter.date)};`);
|
|
198
283
|
}
|
|
199
284
|
exportLines.push(`export const metadata = ${JSON.stringify(frontmatter)};`);
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
import { createElement } from '@emberkit/core';
|
|
210
|
-
|
|
211
|
-
${exportLines.join('\n')}
|
|
212
|
-
|
|
213
|
-
${codeBlockComponents}
|
|
214
|
-
${componentsOverride}
|
|
215
|
-
|
|
216
|
-
${compiledCode}
|
|
217
|
-
|
|
218
|
-
function _GfmTable(props) {
|
|
219
|
-
return createElement('div', { className: 'table-wrapper' },
|
|
220
|
-
createElement('table', { className: 'gfm-table' }, props.children)
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function _GfmUl(props) {
|
|
225
|
-
return createElement('ul', { className: 'task-list' }, props.children);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function _GfmLi(props) {
|
|
229
|
-
return createElement('li', { className: 'task-item' }, props.children);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function _GfmDel(props) {
|
|
233
|
-
return createElement('span', { className: 'strikethrough' }, props.children);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function _GfmSup(props) {
|
|
237
|
-
return createElement('span', { className: 'footnote-ref' }, props.children);
|
|
238
|
-
}
|
|
239
|
-
|
|
285
|
+
let body = typeof compiled === 'object' && compiled !== null && 'value' in compiled
|
|
286
|
+
? String(compiled.value)
|
|
287
|
+
: String(compiled);
|
|
288
|
+
// Rename the @mdx-js/mdx default export so we can wrap it with the
|
|
289
|
+
// md-doc / md-content styling containers that the docs CSS targets.
|
|
290
|
+
body = body.replace(/export default function MDXContent/, 'function _MDXContent');
|
|
291
|
+
// Wrapper re-exports the component with the styling containers.
|
|
292
|
+
// We reuse _jsx/_jsxs already imported by the compiled output.
|
|
293
|
+
const wrapper = `
|
|
240
294
|
export default function MDXComponent(props) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
className: 'md-content md-doc',
|
|
248
|
-
'data-file': ${JSON.stringify(id)},
|
|
249
|
-
children: createElement(_MDXContent, {
|
|
250
|
-
...props,
|
|
251
|
-
components: {
|
|
252
|
-
...components,
|
|
253
|
-
table: _GfmTable,
|
|
254
|
-
ul: _GfmUl,
|
|
255
|
-
li: _GfmLi,
|
|
256
|
-
del: _GfmDel,
|
|
257
|
-
sup: _GfmSup,
|
|
258
|
-
}
|
|
295
|
+
var p = props ?? {};
|
|
296
|
+
return _jsx('article', {
|
|
297
|
+
className: 'md-doc',
|
|
298
|
+
children: _jsx('div', {
|
|
299
|
+
className: 'md-content',
|
|
300
|
+
children: _jsx(_MDXContent, p)
|
|
259
301
|
})
|
|
260
302
|
});
|
|
261
303
|
}
|
|
262
304
|
`;
|
|
263
|
-
return { code:
|
|
305
|
+
return { code: insertAfterLeadingImports(body + wrapper, exportLines.join('\n')) };
|
|
264
306
|
}
|
|
265
307
|
function parseFrontmatter(content) {
|
|
266
308
|
const result = {};
|
|
@@ -336,10 +378,8 @@ function renderCodeBlock(lang, code) {
|
|
|
336
378
|
else if (lang === 'css' || lang === 'scss' || lang === 'sass' || lang === 'less') {
|
|
337
379
|
highlighted = highlightCSS(highlighted);
|
|
338
380
|
}
|
|
339
|
-
else if (lang === 'markdown' || lang === 'md') {
|
|
340
|
-
highlighted = highlightMarkdown(highlighted);
|
|
341
|
-
}
|
|
342
381
|
else {
|
|
382
|
+
// Markdown (and unknown) fenced blocks: literal source only — do not tokenize as markdown/HTML.
|
|
343
383
|
highlighted = escapeHtml(highlighted);
|
|
344
384
|
}
|
|
345
385
|
const langAttr = lang ? ` data-lang="${lang}"` : '';
|
|
@@ -895,35 +935,6 @@ function highlightCSS(code) {
|
|
|
895
935
|
}
|
|
896
936
|
return tokens.join('');
|
|
897
937
|
}
|
|
898
|
-
function highlightMarkdown(code) {
|
|
899
|
-
return code
|
|
900
|
-
.split('\n')
|
|
901
|
-
.map((line) => {
|
|
902
|
-
const escaped = escapeHtml(line);
|
|
903
|
-
// Frontmatter delimiter
|
|
904
|
-
if (/^---$/.test(line))
|
|
905
|
-
return `<span class="op">${escaped}</span>`;
|
|
906
|
-
// Headings
|
|
907
|
-
const headingM = line.match(/^(#{1,6})\s(.+)/);
|
|
908
|
-
if (headingM) {
|
|
909
|
-
return `<span class="kw">${escapeHtml(headingM[1])}</span> <span class="tag">${escapeHtml(headingM[2])}</span>`;
|
|
910
|
-
}
|
|
911
|
-
// Bold / italic markers (keep simple — just colour the line)
|
|
912
|
-
if (/^\s*[-*+]\s/.test(line)) {
|
|
913
|
-
return `<span class="op">${escapeHtml(line.match(/^(\s*[-*+])/)[1])}</span>${escapeHtml(line.slice(line.match(/^(\s*[-*+])/)[1].length))}`;
|
|
914
|
-
}
|
|
915
|
-
// Blockquote
|
|
916
|
-
if (/^>/.test(line))
|
|
917
|
-
return `<span class="cm">${escaped}</span>`;
|
|
918
|
-
// Frontmatter key: value
|
|
919
|
-
const fmM = line.match(/^([\w-]+):\s*(.*)/);
|
|
920
|
-
if (fmM) {
|
|
921
|
-
return `<span class="attr">${escapeHtml(fmM[1])}</span><span class="op">:</span> <span class="str">${escapeHtml(fmM[2])}</span>`;
|
|
922
|
-
}
|
|
923
|
-
return escaped;
|
|
924
|
-
})
|
|
925
|
-
.join('\n');
|
|
926
|
-
}
|
|
927
938
|
function processTables(html) {
|
|
928
939
|
const lines = html.split('\n');
|
|
929
940
|
const result = [];
|
|
@@ -1087,6 +1098,190 @@ function processParagraphs(html, breaks) {
|
|
|
1087
1098
|
})
|
|
1088
1099
|
.join('');
|
|
1089
1100
|
}
|
|
1101
|
+
function generateSSREntry() {
|
|
1102
|
+
return `
|
|
1103
|
+
import { routes } from 'virtual:emberkit-routes';
|
|
1104
|
+
import { createElement } from '@emberkit/core';
|
|
1105
|
+
|
|
1106
|
+
const matchRoute = (routes, pathname) => {
|
|
1107
|
+
const normalizedPath = pathname.replace(/\\/+$/, '') || '/';
|
|
1108
|
+
|
|
1109
|
+
for (const route of routes) {
|
|
1110
|
+
const pattern = routeToRegex(route.path);
|
|
1111
|
+
const match = normalizedPath.match(pattern.regex);
|
|
1112
|
+
if (match) {
|
|
1113
|
+
const params = {};
|
|
1114
|
+
pattern.paramNames.forEach((name, i) => {
|
|
1115
|
+
params[name] = match[i + 1];
|
|
1116
|
+
});
|
|
1117
|
+
return { route, params };
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
return null;
|
|
1121
|
+
};
|
|
1122
|
+
|
|
1123
|
+
const routeToRegex = (routePath) => {
|
|
1124
|
+
const paramNames = [];
|
|
1125
|
+
const regexStr = routePath
|
|
1126
|
+
.replace(/:([^/]+)\\*/g, (_, name) => {
|
|
1127
|
+
paramNames.push(name);
|
|
1128
|
+
return '(.*)';
|
|
1129
|
+
})
|
|
1130
|
+
.replace(/:([^/]+)/g, (_, name) => {
|
|
1131
|
+
paramNames.push(name);
|
|
1132
|
+
return '([^/]+)';
|
|
1133
|
+
});
|
|
1134
|
+
return { regex: new RegExp('^' + regexStr + '$'), paramNames };
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
const renderToString = (element) => {
|
|
1138
|
+
if (!element && element !== 0) return '';
|
|
1139
|
+
if (typeof element === 'string') return escapeHtml(element);
|
|
1140
|
+
if (typeof element === 'number') return String(element);
|
|
1141
|
+
if (Array.isArray(element)) return element.map(renderToString).join('');
|
|
1142
|
+
|
|
1143
|
+
if (typeof element !== 'object' || !element.type) return '';
|
|
1144
|
+
|
|
1145
|
+
let { type, props } = element;
|
|
1146
|
+
props = props || {};
|
|
1147
|
+
|
|
1148
|
+
// Resolve function components
|
|
1149
|
+
let depth = 0;
|
|
1150
|
+
while (typeof type === 'function' && depth < 50) {
|
|
1151
|
+
depth++;
|
|
1152
|
+
try {
|
|
1153
|
+
const result = type(props);
|
|
1154
|
+
if (result && typeof result === 'object' && result.type) {
|
|
1155
|
+
type = result.type;
|
|
1156
|
+
props = result.props || {};
|
|
1157
|
+
} else if (typeof result === 'string' || typeof result === 'number') {
|
|
1158
|
+
return typeof result === 'string' ? escapeHtml(result) : String(result);
|
|
1159
|
+
} else if (Array.isArray(result)) {
|
|
1160
|
+
return result.map(renderToString).join('');
|
|
1161
|
+
} else {
|
|
1162
|
+
return '';
|
|
1163
|
+
}
|
|
1164
|
+
} catch (e) {
|
|
1165
|
+
console.error('[SSR render error]', e);
|
|
1166
|
+
return '';
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
if (type === 'Fragment' || type === 'React.Fragment') {
|
|
1171
|
+
const children = Array.isArray(props.children) ? props.children : [props.children];
|
|
1172
|
+
return children.filter(Boolean).map(renderToString).join('');
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
const SELF_CLOSING = new Set(['area','base','br','col','embed','hr','img','input','link','meta','source','track','wbr']);
|
|
1176
|
+
|
|
1177
|
+
const children = Array.isArray(props.children) ? props.children : (props.children ? [props.children] : []);
|
|
1178
|
+
let childHtml = children.filter(c => c != null).map(renderToString).join('');
|
|
1179
|
+
|
|
1180
|
+
// Handle dangerouslySetInnerHTML
|
|
1181
|
+
if (props.dangerouslySetInnerHTML && props.dangerouslySetInnerHTML.__html) {
|
|
1182
|
+
childHtml = props.dangerouslySetInnerHTML.__html;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
const attrs = Object.entries(props)
|
|
1186
|
+
.filter(([k, v]) => k !== 'children' && k !== 'key' && k !== 'dangerouslySetInnerHTML' && v != null && typeof v !== 'function')
|
|
1187
|
+
.map(([k, v]) => {
|
|
1188
|
+
if (k === 'className') k = 'class';
|
|
1189
|
+
if (v === true) return ' ' + k;
|
|
1190
|
+
if (v === false) return '';
|
|
1191
|
+
if (k === 'style' && typeof v === 'object') {
|
|
1192
|
+
const styleStr = Object.entries(v)
|
|
1193
|
+
.filter(([, sv]) => sv != null)
|
|
1194
|
+
.map(([sp, sv]) => sp.replace(/([A-Z])/g, '-$1').toLowerCase() + ': ' + sv)
|
|
1195
|
+
.join('; ');
|
|
1196
|
+
return ' ' + k + '="' + escapeHtml(styleStr) + '"';
|
|
1197
|
+
}
|
|
1198
|
+
return ' ' + k + '="' + escapeHtml(String(v)) + '"';
|
|
1199
|
+
})
|
|
1200
|
+
.join('');
|
|
1201
|
+
|
|
1202
|
+
if (SELF_CLOSING.has(type)) {
|
|
1203
|
+
return '<' + type + attrs + '/>';
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
return '<' + type + attrs + '>' + childHtml + '</' + type + '>';
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1209
|
+
const escapeHtml = (str) => {
|
|
1210
|
+
if (typeof str !== 'string') return str;
|
|
1211
|
+
return str
|
|
1212
|
+
.replace(/&/g, '&')
|
|
1213
|
+
.replace(/</g, '<')
|
|
1214
|
+
.replace(/>/g, '>')
|
|
1215
|
+
.replace(/"/g, '"')
|
|
1216
|
+
.replace(/'/g, ''');
|
|
1217
|
+
};
|
|
1218
|
+
|
|
1219
|
+
export async function render(url, server) {
|
|
1220
|
+
const pathname = url.split('?')[0];
|
|
1221
|
+
|
|
1222
|
+
// Sort routes: static first, then dynamic
|
|
1223
|
+
const sortedRoutes = [...routes].sort((a, b) => {
|
|
1224
|
+
const aScore = a.path.includes(':') ? 0 : 1;
|
|
1225
|
+
const bScore = b.path.includes(':') ? 0 : 1;
|
|
1226
|
+
return bScore - aScore;
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
const match = matchRoute(sortedRoutes, pathname);
|
|
1230
|
+
|
|
1231
|
+
let appHtml = '';
|
|
1232
|
+
let headContent = '';
|
|
1233
|
+
|
|
1234
|
+
if (match) {
|
|
1235
|
+
try {
|
|
1236
|
+
const mod = await match.route.component();
|
|
1237
|
+
const Component = mod.default || mod;
|
|
1238
|
+
|
|
1239
|
+
// Get metadata if available
|
|
1240
|
+
if (mod.metadata) {
|
|
1241
|
+
if (mod.metadata.title) {
|
|
1242
|
+
headContent += '<title>' + escapeHtml(mod.metadata.title) + '</title>\\n';
|
|
1243
|
+
}
|
|
1244
|
+
if (mod.metadata.description) {
|
|
1245
|
+
headContent += '<meta name="description" content="' + escapeHtml(mod.metadata.description) + '">\\n';
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
const element = createElement(Component, { params: match.params });
|
|
1250
|
+
appHtml = renderToString(element);
|
|
1251
|
+
} catch (e) {
|
|
1252
|
+
console.error('[SSR] Failed to render route:', pathname, e);
|
|
1253
|
+
appHtml = '<div style="color: red; padding: 20px;">SSR Error: ' + escapeHtml(String(e)) + '</div>';
|
|
1254
|
+
}
|
|
1255
|
+
} else {
|
|
1256
|
+
appHtml = '<div style="padding: 20px;">404 - Page not found</div>';
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// Load and transform index.html
|
|
1260
|
+
const fs = await import('node:fs');
|
|
1261
|
+
const path = await import('node:path');
|
|
1262
|
+
const indexPath = path.join(server.config.root, 'index.html');
|
|
1263
|
+
let template = fs.readFileSync(indexPath, 'utf-8');
|
|
1264
|
+
template = await server.transformIndexHtml(url, template);
|
|
1265
|
+
|
|
1266
|
+
// Inject SSR content
|
|
1267
|
+
// Look for body with id="app" or div with id="app"
|
|
1268
|
+
if (template.includes('<body id="app">')) {
|
|
1269
|
+
template = template.replace('<body id="app">', '<body id="app">' + appHtml);
|
|
1270
|
+
} else if (template.includes('<div id="app">')) {
|
|
1271
|
+
template = template.replace('<div id="app"></div>', '<div id="app">' + appHtml + '</div>');
|
|
1272
|
+
} else if (template.includes('<div id="app"/>')) {
|
|
1273
|
+
template = template.replace('<div id="app"/>', '<div id="app">' + appHtml + '</div>');
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// Inject head content if any
|
|
1277
|
+
if (headContent && template.includes('</head>')) {
|
|
1278
|
+
template = template.replace('</head>', headContent + '</head>');
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
return template;
|
|
1282
|
+
}
|
|
1283
|
+
`;
|
|
1284
|
+
}
|
|
1090
1285
|
function scanRouteFiles(dir) {
|
|
1091
1286
|
const files = [];
|
|
1092
1287
|
const extensions = new Set(['tsx', 'ts', 'jsx', 'js', 'md', 'mdx']);
|