@nuraly/lumenjs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +297 -0
  2. package/dist/build/build.d.ts +5 -0
  3. package/dist/build/build.js +172 -0
  4. package/dist/build/error-page.d.ts +1 -0
  5. package/dist/build/error-page.js +74 -0
  6. package/dist/build/scan.d.ts +21 -0
  7. package/dist/build/scan.js +93 -0
  8. package/dist/build/serve-api.d.ts +3 -0
  9. package/dist/build/serve-api.js +56 -0
  10. package/dist/build/serve-loaders.d.ts +4 -0
  11. package/dist/build/serve-loaders.js +115 -0
  12. package/dist/build/serve-ssr.d.ts +7 -0
  13. package/dist/build/serve-ssr.js +121 -0
  14. package/dist/build/serve-static.d.ts +6 -0
  15. package/dist/build/serve-static.js +80 -0
  16. package/dist/build/serve.d.ts +5 -0
  17. package/dist/build/serve.js +79 -0
  18. package/dist/cli.d.ts +2 -0
  19. package/dist/cli.js +65 -0
  20. package/dist/dev-server/config.d.ts +25 -0
  21. package/dist/dev-server/config.js +55 -0
  22. package/dist/dev-server/index-html.d.ts +16 -0
  23. package/dist/dev-server/index-html.js +46 -0
  24. package/dist/dev-server/nuralyui-aliases.d.ts +16 -0
  25. package/dist/dev-server/nuralyui-aliases.js +164 -0
  26. package/dist/dev-server/plugins/vite-plugin-api-routes.d.ts +23 -0
  27. package/dist/dev-server/plugins/vite-plugin-api-routes.js +250 -0
  28. package/dist/dev-server/plugins/vite-plugin-auto-import.d.ts +5 -0
  29. package/dist/dev-server/plugins/vite-plugin-auto-import.js +47 -0
  30. package/dist/dev-server/plugins/vite-plugin-lit-dedup.d.ts +5 -0
  31. package/dist/dev-server/plugins/vite-plugin-lit-dedup.js +62 -0
  32. package/dist/dev-server/plugins/vite-plugin-lit-hmr.d.ts +5 -0
  33. package/dist/dev-server/plugins/vite-plugin-lit-hmr.js +46 -0
  34. package/dist/dev-server/plugins/vite-plugin-loaders.d.ts +38 -0
  35. package/dist/dev-server/plugins/vite-plugin-loaders.js +320 -0
  36. package/dist/dev-server/plugins/vite-plugin-routes.d.ts +21 -0
  37. package/dist/dev-server/plugins/vite-plugin-routes.js +157 -0
  38. package/dist/dev-server/plugins/vite-plugin-source-annotator.d.ts +5 -0
  39. package/dist/dev-server/plugins/vite-plugin-source-annotator.js +39 -0
  40. package/dist/dev-server/plugins/vite-plugin-virtual-modules.d.ts +5 -0
  41. package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +38 -0
  42. package/dist/dev-server/server.d.ts +23 -0
  43. package/dist/dev-server/server.js +155 -0
  44. package/dist/dev-server/ssr-render.d.ts +20 -0
  45. package/dist/dev-server/ssr-render.js +170 -0
  46. package/dist/editor/click-select.d.ts +1 -0
  47. package/dist/editor/click-select.js +46 -0
  48. package/dist/editor/editor-bridge.d.ts +17 -0
  49. package/dist/editor/editor-bridge.js +101 -0
  50. package/dist/editor/element-annotator.d.ts +33 -0
  51. package/dist/editor/element-annotator.js +83 -0
  52. package/dist/editor/hover-detect.d.ts +1 -0
  53. package/dist/editor/hover-detect.js +36 -0
  54. package/dist/editor/inline-text-edit.d.ts +1 -0
  55. package/dist/editor/inline-text-edit.js +114 -0
  56. package/dist/integrations/add.d.ts +1 -0
  57. package/dist/integrations/add.js +89 -0
  58. package/dist/runtime/app-shell.d.ts +1 -0
  59. package/dist/runtime/app-shell.js +22 -0
  60. package/dist/runtime/response.d.ts +15 -0
  61. package/dist/runtime/response.js +13 -0
  62. package/dist/runtime/router-data.d.ts +3 -0
  63. package/dist/runtime/router-data.js +40 -0
  64. package/dist/runtime/router-hydration.d.ts +10 -0
  65. package/dist/runtime/router-hydration.js +68 -0
  66. package/dist/runtime/router.d.ts +35 -0
  67. package/dist/runtime/router.js +202 -0
  68. package/dist/shared/dom-shims.d.ts +5 -0
  69. package/dist/shared/dom-shims.js +63 -0
  70. package/dist/shared/route-matching.d.ts +6 -0
  71. package/dist/shared/route-matching.js +44 -0
  72. package/dist/shared/types.d.ts +16 -0
  73. package/dist/shared/types.js +1 -0
  74. package/dist/shared/utils.d.ts +42 -0
  75. package/dist/shared/utils.js +109 -0
  76. package/package.json +53 -0
@@ -0,0 +1,56 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { readBody } from '../shared/utils.js';
4
+ import { matchRoute } from '../shared/route-matching.js';
5
+ export async function handleApiRoute(manifest, serverDir, pathname, queryString, method, req, res) {
6
+ const matched = matchRoute(manifest.apiRoutes, pathname);
7
+ if (!matched) {
8
+ res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
9
+ res.end(JSON.stringify({ error: 'API route not found' }));
10
+ return;
11
+ }
12
+ const modulePath = path.join(serverDir, matched.route.module);
13
+ if (!fs.existsSync(modulePath)) {
14
+ res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
15
+ res.end(JSON.stringify({ error: 'API route module not found' }));
16
+ return;
17
+ }
18
+ const mod = await import(modulePath);
19
+ const handler = mod[method];
20
+ if (!handler || typeof handler !== 'function') {
21
+ res.writeHead(405, { 'Content-Type': 'application/json; charset=utf-8' });
22
+ res.end(JSON.stringify({ error: `Method ${method} not allowed` }));
23
+ return;
24
+ }
25
+ // Parse query
26
+ const query = {};
27
+ if (queryString) {
28
+ for (const pair of queryString.split('&')) {
29
+ const [key, val] = pair.split('=');
30
+ query[decodeURIComponent(key)] = decodeURIComponent(val || '');
31
+ }
32
+ }
33
+ // Parse body for non-GET methods
34
+ let body = undefined;
35
+ if (method !== 'GET' && method !== 'HEAD') {
36
+ body = await readBody(req);
37
+ }
38
+ try {
39
+ const result = await handler({
40
+ method,
41
+ url: pathname,
42
+ query,
43
+ params: matched.params,
44
+ body,
45
+ headers: req.headers,
46
+ });
47
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
48
+ res.end(JSON.stringify(result));
49
+ }
50
+ catch (err) {
51
+ const status = err?.status || 500;
52
+ const message = err?.message || 'Internal server error';
53
+ res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
54
+ res.end(JSON.stringify({ error: message }));
55
+ }
56
+ }
@@ -0,0 +1,4 @@
1
+ import http from 'http';
2
+ import type { BuildManifest } from '../shared/types.js';
3
+ export declare function handleLayoutLoaderRequest(manifest: BuildManifest, serverDir: string, queryString: string | undefined, headers: http.IncomingHttpHeaders, res: http.ServerResponse): Promise<void>;
4
+ export declare function handleLoaderRequest(manifest: BuildManifest, serverDir: string, pagesDir: string, pathname: string, queryString: string | undefined, headers: http.IncomingHttpHeaders, res: http.ServerResponse): Promise<void>;
@@ -0,0 +1,115 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { isRedirectResponse } from '../shared/utils.js';
4
+ import { matchRoute } from '../shared/route-matching.js';
5
+ export async function handleLayoutLoaderRequest(manifest, serverDir, queryString, headers, res) {
6
+ const query = {};
7
+ if (queryString) {
8
+ for (const pair of queryString.split('&')) {
9
+ const [key, val] = pair.split('=');
10
+ query[decodeURIComponent(key)] = decodeURIComponent(val || '');
11
+ }
12
+ }
13
+ const dir = query.__dir || '';
14
+ // Find the layout in manifest
15
+ const layout = (manifest.layouts || []).find(l => l.dir === dir);
16
+ if (!layout || !layout.hasLoader || !layout.module) {
17
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
18
+ res.end(JSON.stringify({ __nk_no_loader: true }));
19
+ return;
20
+ }
21
+ const modulePath = path.join(serverDir, layout.module);
22
+ if (!fs.existsSync(modulePath)) {
23
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
24
+ res.end(JSON.stringify({ __nk_no_loader: true }));
25
+ return;
26
+ }
27
+ try {
28
+ const mod = await import(modulePath);
29
+ if (!mod.loader || typeof mod.loader !== 'function') {
30
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
31
+ res.end(JSON.stringify({ __nk_no_loader: true }));
32
+ return;
33
+ }
34
+ const result = await mod.loader({ params: {}, query: {}, url: `/__layout/${dir}`, headers });
35
+ if (isRedirectResponse(result)) {
36
+ res.writeHead(result.status || 302, { Location: result.location });
37
+ res.end();
38
+ return;
39
+ }
40
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
41
+ res.end(JSON.stringify(result ?? null));
42
+ }
43
+ catch (err) {
44
+ if (isRedirectResponse(err)) {
45
+ res.writeHead(err.status || 302, { Location: err.location });
46
+ res.end();
47
+ return;
48
+ }
49
+ console.error(`[LumenJS] Layout loader error for dir=${dir}:`, err);
50
+ const status = err?.status || 500;
51
+ const message = err?.message || 'Layout loader failed';
52
+ res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
53
+ res.end(JSON.stringify({ error: message }));
54
+ }
55
+ }
56
+ export async function handleLoaderRequest(manifest, serverDir, pagesDir, pathname, queryString, headers, res) {
57
+ const pagePath = pathname.replace('/__nk_loader', '') || '/';
58
+ // Parse query params
59
+ const query = {};
60
+ if (queryString) {
61
+ for (const pair of queryString.split('&')) {
62
+ const [key, val] = pair.split('=');
63
+ query[decodeURIComponent(key)] = decodeURIComponent(val || '');
64
+ }
65
+ }
66
+ let params = {};
67
+ if (query.__params) {
68
+ try {
69
+ params = JSON.parse(query.__params);
70
+ }
71
+ catch { /* ignore */ }
72
+ delete query.__params;
73
+ }
74
+ // Find the matching route with a loader
75
+ const matched = matchRoute(manifest.routes.filter(r => r.hasLoader), pagePath);
76
+ if (!matched || !matched.route.module) {
77
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
78
+ res.end(JSON.stringify({ __nk_no_loader: true }));
79
+ return;
80
+ }
81
+ const modulePath = path.join(serverDir, matched.route.module);
82
+ if (!fs.existsSync(modulePath)) {
83
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
84
+ res.end(JSON.stringify({ __nk_no_loader: true }));
85
+ return;
86
+ }
87
+ try {
88
+ const mod = await import(modulePath);
89
+ if (!mod.loader || typeof mod.loader !== 'function') {
90
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
91
+ res.end(JSON.stringify({ __nk_no_loader: true }));
92
+ return;
93
+ }
94
+ const result = await mod.loader({ params: matched.params, query, url: pagePath, headers });
95
+ if (isRedirectResponse(result)) {
96
+ res.writeHead(result.status || 302, { Location: result.location });
97
+ res.end();
98
+ return;
99
+ }
100
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
101
+ res.end(JSON.stringify(result ?? null));
102
+ }
103
+ catch (err) {
104
+ if (isRedirectResponse(err)) {
105
+ res.writeHead(err.status || 302, { Location: err.location });
106
+ res.end();
107
+ return;
108
+ }
109
+ console.error(`[LumenJS] Loader error for ${pagePath}:`, err);
110
+ const status = err?.status || 500;
111
+ const message = err?.message || 'Loader failed';
112
+ res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
113
+ res.end(JSON.stringify({ error: message }));
114
+ }
115
+ }
@@ -0,0 +1,7 @@
1
+ import http from 'http';
2
+ import type { BuildManifest } from '../shared/types.js';
3
+ export declare function handlePageRoute(manifest: BuildManifest, serverDir: string, pagesDir: string, pathname: string, queryString: string | undefined, indexHtmlShell: string, title: string, ssrRuntime: {
4
+ render: any;
5
+ html: any;
6
+ unsafeStatic: any;
7
+ } | null, req: http.IncomingMessage, res: http.ServerResponse): Promise<void>;
@@ -0,0 +1,121 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { stripOuterLitMarkers, dirToLayoutTagName, findTagName, isRedirectResponse } from '../shared/utils.js';
4
+ import { matchRoute } from '../shared/route-matching.js';
5
+ import { sendCompressed } from './serve-static.js';
6
+ export async function handlePageRoute(manifest, serverDir, pagesDir, pathname, queryString, indexHtmlShell, title, ssrRuntime, req, res) {
7
+ // Find matching route (any route, not just those with loaders)
8
+ const allMatched = matchRoute(manifest.routes, pathname);
9
+ // Try SSR for routes with loaders
10
+ const matched = matchRoute(manifest.routes.filter(r => r.hasLoader), pathname);
11
+ if (matched && matched.route.module) {
12
+ const modulePath = path.join(serverDir, matched.route.module);
13
+ if (fs.existsSync(modulePath)) {
14
+ try {
15
+ const mod = await import(modulePath);
16
+ // Run loader
17
+ let loaderData = undefined;
18
+ if (mod.loader && typeof mod.loader === 'function') {
19
+ loaderData = await mod.loader({ params: matched.params, query: {}, url: pathname, headers: req.headers });
20
+ if (isRedirectResponse(loaderData)) {
21
+ res.writeHead(loaderData.status || 302, { Location: loaderData.location });
22
+ res.end();
23
+ return;
24
+ }
25
+ }
26
+ // Find tag name from module
27
+ const tagName = findTagName(mod);
28
+ // Run layout loaders
29
+ const layoutDirs = (allMatched || matched).route.layouts || [];
30
+ const layoutsData = [];
31
+ const layoutModules = [];
32
+ for (const dir of layoutDirs) {
33
+ const layout = (manifest.layouts || []).find(l => l.dir === dir);
34
+ if (!layout)
35
+ continue;
36
+ let layoutLoaderData = undefined;
37
+ if (layout.hasLoader && layout.module) {
38
+ const layoutModulePath = path.join(serverDir, layout.module);
39
+ if (fs.existsSync(layoutModulePath)) {
40
+ const layoutMod = await import(layoutModulePath);
41
+ if (layoutMod.loader && typeof layoutMod.loader === 'function') {
42
+ layoutLoaderData = await layoutMod.loader({ params: {}, query: {}, url: pathname, headers: req.headers });
43
+ if (isRedirectResponse(layoutLoaderData)) {
44
+ res.writeHead(layoutLoaderData.status || 302, { Location: layoutLoaderData.location });
45
+ res.end();
46
+ return;
47
+ }
48
+ }
49
+ }
50
+ }
51
+ const layoutTagName = dirToLayoutTagName(dir);
52
+ layoutsData.push({ loaderPath: dir, data: layoutLoaderData });
53
+ layoutModules.push({ tagName: layoutTagName, loaderData: layoutLoaderData });
54
+ }
55
+ if (tagName && ssrRuntime) {
56
+ // SSR render with the bundled @lit-labs/ssr runtime
57
+ try {
58
+ const { render, html, unsafeStatic } = ssrRuntime;
59
+ const pageTag = unsafeStatic(tagName);
60
+ const pageTemplate = html `<${pageTag} .loaderData=${loaderData}></${pageTag}>`;
61
+ let ssrHtml = '';
62
+ for (const chunk of render(pageTemplate)) {
63
+ ssrHtml += typeof chunk === 'string' ? chunk : String(chunk);
64
+ }
65
+ ssrHtml = stripOuterLitMarkers(ssrHtml);
66
+ for (let i = layoutModules.length - 1; i >= 0; i--) {
67
+ const lTag = unsafeStatic(layoutModules[i].tagName);
68
+ const lData = layoutModules[i].loaderData;
69
+ const lTemplate = html `<${lTag} .loaderData=${lData}></${lTag}>`;
70
+ let lHtml = '';
71
+ for (const chunk of render(lTemplate)) {
72
+ lHtml += typeof chunk === 'string' ? chunk : String(chunk);
73
+ }
74
+ if (i > 0) {
75
+ lHtml = stripOuterLitMarkers(lHtml);
76
+ }
77
+ const closingTag = `</${layoutModules[i].tagName}>`;
78
+ const closingIdx = lHtml.lastIndexOf(closingTag);
79
+ if (closingIdx !== -1) {
80
+ ssrHtml = lHtml.slice(0, closingIdx) + ssrHtml + lHtml.slice(closingIdx);
81
+ }
82
+ else {
83
+ ssrHtml = lHtml + ssrHtml;
84
+ }
85
+ }
86
+ // Build SSR data script
87
+ const ssrDataObj = layoutsData.length > 0
88
+ ? { page: loaderData, layouts: layoutsData }
89
+ : loaderData;
90
+ const loaderDataScript = ssrDataObj !== undefined
91
+ ? `<script type="application/json" id="__nk_ssr_data__">${JSON.stringify(ssrDataObj).replace(/</g, '\\u003c')}</script>`
92
+ : '';
93
+ const hydrateScript = `<script type="module">import '@lit-labs/ssr-client/lit-element-hydrate-support.js';</script>`;
94
+ let html_out = indexHtmlShell;
95
+ html_out = html_out.replace(/<nk-app><\/nk-app>/, `${loaderDataScript}<nk-app data-nk-ssr><div id="nk-router-outlet">${ssrHtml}</div></nk-app>${hydrateScript}`);
96
+ sendCompressed(req, res, 200, 'text/html; charset=utf-8', html_out);
97
+ return;
98
+ }
99
+ catch (ssrErr) {
100
+ console.error('[LumenJS] SSR render failed, falling back to CSR:', ssrErr);
101
+ }
102
+ }
103
+ // Fallback: inject loader data without SSR HTML
104
+ if (loaderData !== undefined || layoutsData.length > 0) {
105
+ const ssrDataObj = layoutsData.length > 0
106
+ ? { page: loaderData, layouts: layoutsData }
107
+ : loaderData;
108
+ const loaderDataScript = `<script type="application/json" id="__nk_ssr_data__">${JSON.stringify(ssrDataObj).replace(/</g, '\\u003c')}</script>`;
109
+ let html_out = indexHtmlShell.replace('<nk-app>', `${loaderDataScript}<nk-app>`);
110
+ sendCompressed(req, res, 200, 'text/html; charset=utf-8', html_out);
111
+ return;
112
+ }
113
+ }
114
+ catch (err) {
115
+ console.error('[LumenJS] Page handler error:', err);
116
+ }
117
+ }
118
+ }
119
+ // SPA fallback — serve the built index.html
120
+ sendCompressed(req, res, 200, 'text/html; charset=utf-8', indexHtmlShell);
121
+ }
@@ -0,0 +1,6 @@
1
+ import http from 'http';
2
+ export declare const MIME_TYPES: Record<string, string>;
3
+ export declare function isCompressible(contentType: string): boolean;
4
+ export declare function acceptsGzip(req: http.IncomingMessage): boolean;
5
+ export declare function sendCompressed(req: http.IncomingMessage, res: http.ServerResponse, statusCode: number, contentType: string, body: string | Buffer): void;
6
+ export declare function serveStaticFile(clientDir: string, pathname: string, req: http.IncomingMessage, res: http.ServerResponse): boolean;
@@ -0,0 +1,80 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { createGzip } from 'zlib';
4
+ import { pipeline } from 'stream';
5
+ export const MIME_TYPES = {
6
+ '.html': 'text/html; charset=utf-8',
7
+ '.js': 'application/javascript; charset=utf-8',
8
+ '.mjs': 'application/javascript; charset=utf-8',
9
+ '.css': 'text/css; charset=utf-8',
10
+ '.json': 'application/json; charset=utf-8',
11
+ '.png': 'image/png',
12
+ '.jpg': 'image/jpeg',
13
+ '.jpeg': 'image/jpeg',
14
+ '.gif': 'image/gif',
15
+ '.svg': 'image/svg+xml',
16
+ '.ico': 'image/x-icon',
17
+ '.webp': 'image/webp',
18
+ '.woff': 'font/woff',
19
+ '.woff2': 'font/woff2',
20
+ '.ttf': 'font/ttf',
21
+ '.eot': 'application/vnd.ms-fontobject',
22
+ '.otf': 'font/otf',
23
+ '.map': 'application/json',
24
+ '.txt': 'text/plain; charset=utf-8',
25
+ '.xml': 'application/xml',
26
+ '.webmanifest': 'application/manifest+json',
27
+ };
28
+ const COMPRESSIBLE_TYPES = new Set([
29
+ 'text/html', 'text/css', 'text/plain', 'text/xml',
30
+ 'application/javascript', 'application/json', 'application/xml',
31
+ 'application/manifest+json', 'image/svg+xml',
32
+ ]);
33
+ export function isCompressible(contentType) {
34
+ const base = contentType.split(';')[0].trim();
35
+ return COMPRESSIBLE_TYPES.has(base);
36
+ }
37
+ export function acceptsGzip(req) {
38
+ const ae = req.headers['accept-encoding'];
39
+ return typeof ae === 'string' && ae.includes('gzip');
40
+ }
41
+ export function sendCompressed(req, res, statusCode, contentType, body) {
42
+ if (acceptsGzip(req) && isCompressible(contentType) && Buffer.byteLength(body) > 1024) {
43
+ res.writeHead(statusCode, { 'Content-Type': contentType, 'Content-Encoding': 'gzip', 'Vary': 'Accept-Encoding' });
44
+ const gzip = createGzip();
45
+ pipeline(gzip, res, () => { });
46
+ gzip.end(body);
47
+ }
48
+ else {
49
+ res.writeHead(statusCode, { 'Content-Type': contentType });
50
+ res.end(body);
51
+ }
52
+ }
53
+ export function serveStaticFile(clientDir, pathname, req, res) {
54
+ // Prevent directory traversal
55
+ const safePath = path.normalize(pathname).replace(/^(\.\.[/\\])+/, '');
56
+ const filePath = path.join(clientDir, safePath);
57
+ if (!filePath.startsWith(clientDir)) {
58
+ return false;
59
+ }
60
+ if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) {
61
+ return false;
62
+ }
63
+ const ext = path.extname(filePath).toLowerCase();
64
+ const contentType = MIME_TYPES[ext] || 'application/octet-stream';
65
+ const cacheControl = pathname.includes('/assets/') && /\.[a-f0-9]{8,}\./.test(pathname)
66
+ ? 'public, max-age=31536000, immutable'
67
+ : 'public, max-age=3600';
68
+ const content = fs.readFileSync(filePath);
69
+ if (acceptsGzip(req) && isCompressible(contentType) && content.length > 1024) {
70
+ res.writeHead(200, { 'Content-Type': contentType, 'Cache-Control': cacheControl, 'Content-Encoding': 'gzip', 'Vary': 'Accept-Encoding' });
71
+ const gzip = createGzip();
72
+ pipeline(gzip, res, () => { });
73
+ gzip.end(content);
74
+ }
75
+ else {
76
+ res.writeHead(200, { 'Content-Type': contentType, 'Cache-Control': cacheControl });
77
+ res.end(content);
78
+ }
79
+ return true;
80
+ }
@@ -0,0 +1,5 @@
1
+ export interface ServeOptions {
2
+ projectDir: string;
3
+ port?: number;
4
+ }
5
+ export declare function serveProject(options: ServeOptions): Promise<void>;
@@ -0,0 +1,79 @@
1
+ import http from 'http';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { readProjectConfig } from '../dev-server/config.js';
5
+ import { installDomShims } from '../shared/dom-shims.js';
6
+ import { serveStaticFile, sendCompressed } from './serve-static.js';
7
+ import { handleApiRoute } from './serve-api.js';
8
+ import { handleLoaderRequest, handleLayoutLoaderRequest } from './serve-loaders.js';
9
+ import { handlePageRoute } from './serve-ssr.js';
10
+ import { renderErrorPage } from './error-page.js';
11
+ export async function serveProject(options) {
12
+ const { projectDir } = options;
13
+ const port = options.port || 3000;
14
+ const outDir = path.join(projectDir, '.lumenjs');
15
+ const clientDir = path.join(outDir, 'client');
16
+ const serverDir = path.join(outDir, 'server');
17
+ const manifestPath = path.join(outDir, 'manifest.json');
18
+ if (!fs.existsSync(manifestPath)) {
19
+ console.error('[LumenJS] No build found. Run `lumenjs build` first.');
20
+ process.exit(1);
21
+ }
22
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
23
+ const { title } = readProjectConfig(projectDir);
24
+ // Read the built index.html shell
25
+ const indexHtmlPath = path.join(clientDir, 'index.html');
26
+ if (!fs.existsSync(indexHtmlPath)) {
27
+ console.error('[LumenJS] No index.html found in build output.');
28
+ process.exit(1);
29
+ }
30
+ const indexHtmlShell = fs.readFileSync(indexHtmlPath, 'utf-8');
31
+ // Load bundled SSR runtime first — it installs @lit-labs/ssr DOM shim
32
+ // which must be in place before any Lit class is instantiated.
33
+ const ssrRuntimePath = path.join(serverDir, 'ssr-runtime.js');
34
+ let ssrRuntime = null;
35
+ if (fs.existsSync(ssrRuntimePath)) {
36
+ ssrRuntime = await import(ssrRuntimePath);
37
+ }
38
+ // Install additional DOM shims that NuralyUI components may need
39
+ installDomShims();
40
+ const pagesDir = path.join(projectDir, 'pages');
41
+ const server = http.createServer(async (req, res) => {
42
+ const url = req.url || '/';
43
+ const [pathname, queryString] = url.split('?');
44
+ const method = req.method || 'GET';
45
+ try {
46
+ // 1. API routes
47
+ if (pathname.startsWith('/api/')) {
48
+ await handleApiRoute(manifest, serverDir, pathname, queryString, method, req, res);
49
+ return;
50
+ }
51
+ // 2. Static assets — try to serve from client dir
52
+ if (pathname.includes('.')) {
53
+ const served = serveStaticFile(clientDir, pathname, req, res);
54
+ if (served)
55
+ return;
56
+ }
57
+ // 3. Layout loader endpoint
58
+ if (pathname === '/__nk_loader/__layout/' || pathname === '/__nk_loader/__layout') {
59
+ await handleLayoutLoaderRequest(manifest, serverDir, queryString, req.headers, res);
60
+ return;
61
+ }
62
+ // 4. Loader endpoint for client-side navigation
63
+ if (pathname.startsWith('/__nk_loader/')) {
64
+ await handleLoaderRequest(manifest, serverDir, pagesDir, pathname, queryString, req.headers, res);
65
+ return;
66
+ }
67
+ // 5. Page routes — SSR render
68
+ await handlePageRoute(manifest, serverDir, pagesDir, pathname, queryString, indexHtmlShell, title, ssrRuntime, req, res);
69
+ }
70
+ catch (err) {
71
+ console.error('[LumenJS] Request error:', err);
72
+ const html = renderErrorPage(500, 'Something went wrong', 'An unexpected error occurred while processing your request.', process.env.NODE_ENV !== 'production' ? err?.stack || err?.message : undefined);
73
+ sendCompressed(req, res, 500, 'text/html; charset=utf-8', html);
74
+ }
75
+ });
76
+ server.listen(port, () => {
77
+ console.log(`[LumenJS] Production server running at http://localhost:${port}`);
78
+ });
79
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ import path from 'path';
3
+ const args = process.argv.slice(2);
4
+ const command = args[0];
5
+ function getArg(name) {
6
+ const idx = args.indexOf(`--${name}`);
7
+ if (idx !== -1 && idx + 1 < args.length)
8
+ return args[idx + 1];
9
+ return undefined;
10
+ }
11
+ const USAGE = `Usage:
12
+ lumenjs dev [--project <dir>] [--port <port>] [--base <path>] [--editor-mode]
13
+ lumenjs build [--project <dir>] [--out <dir>]
14
+ lumenjs serve [--project <dir>] [--port <port>]
15
+ lumenjs add <integration>`;
16
+ if (!command || !['dev', 'build', 'serve', 'add'].includes(command)) {
17
+ console.error(USAGE);
18
+ process.exit(1);
19
+ }
20
+ const projectDir = path.resolve(getArg('project') || '.');
21
+ async function main() {
22
+ if (command === 'dev') {
23
+ const { createDevServer } = await import('./dev-server/server.js');
24
+ const port = parseInt(getArg('port') || '3000', 10);
25
+ const editorMode = args.includes('--editor-mode');
26
+ const base = getArg('base') || '/';
27
+ console.log(`[LumenJS] Starting dev server...`);
28
+ console.log(` Project: ${projectDir}`);
29
+ console.log(` Port: ${port}`);
30
+ if (base !== '/')
31
+ console.log(` Base: ${base}`);
32
+ console.log(` Editor mode: ${editorMode}`);
33
+ const server = await createDevServer({ projectDir, port, editorMode, base });
34
+ await server.listen();
35
+ const address = server.httpServer?.address();
36
+ const actualPort = typeof address === 'object' && address ? address.port : port;
37
+ console.log(`[LumenJS] Dev server running at http://localhost:${actualPort}`);
38
+ }
39
+ else if (command === 'build') {
40
+ const { buildProject } = await import('./build/build.js');
41
+ const outDir = getArg('out');
42
+ console.log(`[LumenJS] Starting production build...`);
43
+ console.log(` Project: ${projectDir}`);
44
+ if (outDir)
45
+ console.log(` Output: ${outDir}`);
46
+ await buildProject({ projectDir, outDir });
47
+ }
48
+ else if (command === 'add') {
49
+ const integration = args[1];
50
+ const { addIntegration } = await import('./integrations/add.js');
51
+ await addIntegration(projectDir, integration);
52
+ }
53
+ else if (command === 'serve') {
54
+ const { serveProject } = await import('./build/serve.js');
55
+ const port = parseInt(getArg('port') || '3000', 10);
56
+ console.log(`[LumenJS] Starting production server...`);
57
+ console.log(` Project: ${projectDir}`);
58
+ console.log(` Port: ${port}`);
59
+ await serveProject({ projectDir, port });
60
+ }
61
+ }
62
+ main().catch(err => {
63
+ console.error('[LumenJS] Failed to start:', err);
64
+ process.exit(1);
65
+ });
@@ -0,0 +1,25 @@
1
+ export interface ProjectConfig {
2
+ title: string;
3
+ integrations: string[];
4
+ }
5
+ /**
6
+ * Reads the project config from lumenjs.config.ts.
7
+ */
8
+ export declare function readProjectConfig(projectDir: string): ProjectConfig;
9
+ /**
10
+ * Reads the project title from lumenjs.config.ts (or returns default).
11
+ * @deprecated Use readProjectConfig() instead.
12
+ */
13
+ export declare function readProjectTitle(projectDir: string): string;
14
+ /**
15
+ * Returns the path to lumenjs's own node_modules.
16
+ */
17
+ export declare function getLumenJSNodeModules(): string;
18
+ /**
19
+ * Returns paths to lumenjs's compiled dist/ runtime and editor directories.
20
+ */
21
+ export declare function getLumenJSDirs(): {
22
+ distDir: string;
23
+ runtimeDir: string;
24
+ editorDir: string;
25
+ };
@@ -0,0 +1,55 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import { fileURLToPath } from 'url';
4
+ const __filename = fileURLToPath(import.meta.url);
5
+ const __dirname = path.dirname(__filename);
6
+ /**
7
+ * Reads the project config from lumenjs.config.ts.
8
+ */
9
+ export function readProjectConfig(projectDir) {
10
+ let title = 'LumenJS App';
11
+ let integrations = [];
12
+ const configPath = path.join(projectDir, 'lumenjs.config.ts');
13
+ if (fs.existsSync(configPath)) {
14
+ try {
15
+ const configContent = fs.readFileSync(configPath, 'utf-8');
16
+ const titleMatch = configContent.match(/title\s*:\s*['"]([^'"]+)['"]/);
17
+ if (titleMatch)
18
+ title = titleMatch[1];
19
+ const intMatch = configContent.match(/integrations\s*:\s*\[([^\]]*)\]/);
20
+ if (intMatch) {
21
+ integrations = intMatch[1]
22
+ .split(',')
23
+ .map(s => s.trim().replace(/^['"]|['"]$/g, ''))
24
+ .filter(Boolean);
25
+ }
26
+ }
27
+ catch { /* use defaults */ }
28
+ }
29
+ return { title, integrations };
30
+ }
31
+ /**
32
+ * Reads the project title from lumenjs.config.ts (or returns default).
33
+ * @deprecated Use readProjectConfig() instead.
34
+ */
35
+ export function readProjectTitle(projectDir) {
36
+ return readProjectConfig(projectDir).title;
37
+ }
38
+ /**
39
+ * Returns the path to lumenjs's own node_modules.
40
+ */
41
+ export function getLumenJSNodeModules() {
42
+ return path.resolve(__dirname, '../../node_modules');
43
+ }
44
+ /**
45
+ * Returns paths to lumenjs's compiled dist/ runtime and editor directories.
46
+ */
47
+ export function getLumenJSDirs() {
48
+ const lumenRoot = path.resolve(__dirname, '../..');
49
+ const distDir = path.join(lumenRoot, 'dist');
50
+ return {
51
+ distDir,
52
+ runtimeDir: path.join(distDir, 'runtime'),
53
+ editorDir: path.join(distDir, 'editor'),
54
+ };
55
+ }
@@ -0,0 +1,16 @@
1
+ export interface IndexHtmlOptions {
2
+ title: string;
3
+ editorMode: boolean;
4
+ ssrContent?: string;
5
+ loaderData?: any;
6
+ layoutsData?: Array<{
7
+ loaderPath: string;
8
+ data: any;
9
+ }>;
10
+ integrations?: string[];
11
+ }
12
+ /**
13
+ * Generates the index.html shell that loads the LumenJS app.
14
+ * Includes the router, app shell, and optionally the editor bridge.
15
+ */
16
+ export declare function generateIndexHtml(options: IndexHtmlOptions): string;