@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 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';
@@ -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,CAmEjE;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"}
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 === next)
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 === next)
40
+ if (equals(value, next))
40
41
  return;
41
42
  value = next;
42
43
  if (subs.size > 0) {
@@ -1,4 +1,5 @@
1
1
  export * from './types.js';
2
2
  export * from './helpers/render-html.js';
3
3
  export * from './helpers/ssr.js';
4
+ export * from './server.js';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -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
@@ -1,3 +1,4 @@
1
1
  export * from './types.js';
2
2
  export * from './helpers/render-html.js';
3
3
  export * from './helpers/ssr.js';
4
+ export * from './server.js';
@@ -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, '&amp;')
36
+ .replace(/</g, '&lt;')
37
+ .replace(/>/g, '&gt;')
38
+ .replace(/"/g, '&quot;')
39
+ .replace(/'/g, '&#039;');
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,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,KAAK,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAoBtE,wBAAgB,kBAAkB,CAAC,WAAW,GAAE,qBAA0B,GAAG,MAAM,CA8FlF;AA8kCD,YAAY,EAAE,qBAAqB,EAAE,YAAY,EAAE,CAAC"}
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
- function resolveConfig(userOptions = {}) {
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
- const options = resolveConfig(userOptions);
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
- async function transformMDX(code, id) {
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
- // Extract code blocks before MDX compilation to preserve syntax
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
- // Build components override object
201
- const componentsOverride = codeBlocks.length > 0
202
- ? `
203
- const _codeBlockComponents = {
204
- ${codeBlocks.map((b) => `CodeBlock_${b.index}`).join(', ')}
205
- };
206
- `
207
- : '';
208
- const componentCode = `
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
- const components = {
242
- ...(props.components || {}),
243
- ${codeBlocks.map((b) => `CodeBlock_${b.index}`).join(', ')}
244
- };
245
-
246
- return createElement('div', {
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: componentCode };
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, '&amp;')
1213
+ .replace(/</g, '&lt;')
1214
+ .replace(/>/g, '&gt;')
1215
+ .replace(/"/g, '&quot;')
1216
+ .replace(/'/g, '&#039;');
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']);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emberkit/core",
3
- "version": "0.3.2",
3
+ "version": "0.3.6",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Lightweight TypeScript-first JSX framework core",