@nuraly/lumenjs 0.1.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -7
- package/dist/auth/native-auth.d.ts +9 -0
- package/dist/auth/native-auth.js +49 -2
- package/dist/auth/routes/login.js +24 -1
- package/dist/auth/routes/totp.d.ts +22 -0
- package/dist/auth/routes/totp.js +232 -0
- package/dist/auth/routes.js +14 -0
- package/dist/auth/token.js +2 -2
- package/dist/build/build-markdown.d.ts +15 -0
- package/dist/build/build-markdown.js +90 -0
- package/dist/build/build-server.d.ts +2 -1
- package/dist/build/build-server.js +12 -4
- package/dist/build/build.js +46 -5
- package/dist/build/scan.d.ts +1 -0
- package/dist/build/scan.js +2 -1
- package/dist/build/serve-static.js +2 -1
- package/dist/build/serve.js +131 -11
- package/dist/dev-server/config.js +18 -1
- package/dist/dev-server/index-html.d.ts +1 -0
- package/dist/dev-server/index-html.js +4 -1
- package/dist/dev-server/plugins/vite-plugin-llms.js +1 -0
- package/dist/dev-server/plugins/vite-plugin-loaders.d.ts +4 -3
- package/dist/dev-server/plugins/vite-plugin-loaders.js +4 -3
- package/dist/dev-server/plugins/vite-plugin-routes.js +3 -2
- package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +34 -6
- package/dist/dev-server/server.js +146 -88
- package/dist/dev-server/ssr-render.js +10 -2
- package/dist/editor/ai/backend.js +11 -2
- package/dist/editor/ai/deepseek-client.d.ts +7 -0
- package/dist/editor/ai/deepseek-client.js +113 -0
- package/dist/editor/ai/opencode-client.d.ts +1 -1
- package/dist/editor/ai/opencode-client.js +21 -47
- package/dist/editor/ai/types.d.ts +1 -1
- package/dist/editor/ai/types.js +2 -2
- package/dist/editor/ai-chat-panel.js +27 -1
- package/dist/editor/editor-bridge.js +2 -1
- package/dist/editor/overlay-hmr.js +2 -1
- package/dist/llms/generate.d.ts +15 -1
- package/dist/llms/generate.js +54 -44
- package/dist/runtime/app-shell.d.ts +1 -1
- package/dist/runtime/app-shell.js +1 -0
- package/dist/runtime/communication.d.ts +65 -36
- package/dist/runtime/communication.js +117 -57
- package/dist/runtime/island.d.ts +16 -0
- package/dist/runtime/island.js +80 -0
- package/dist/runtime/router-hydration.js +9 -2
- package/dist/runtime/router.d.ts +3 -1
- package/dist/runtime/router.js +51 -3
- package/dist/runtime/webrtc.d.ts +44 -0
- package/dist/runtime/webrtc.js +263 -13
- package/dist/shared/dom-shims.js +4 -2
- package/dist/shared/html-to-markdown.d.ts +6 -0
- package/dist/shared/html-to-markdown.js +73 -0
- package/dist/shared/types.d.ts +1 -0
- package/dist/storage/adapters/s3.js +6 -3
- package/package.json +33 -7
- package/templates/blog/pages/index.ts +3 -3
- package/templates/blog/pages/posts/[slug].ts +17 -6
- package/templates/blog/pages/tag/[tag].ts +6 -6
- package/templates/dashboard/pages/index.ts +7 -7
- package/templates/default/pages/index.ts +3 -3
- package/templates/social/api/posts/[id].ts +0 -14
- package/templates/social/api/posts.ts +0 -11
- package/templates/social/api/profile/[username].ts +0 -10
- package/templates/social/api/upload.ts +0 -19
- package/templates/social/data/migrations/001_init.sql +0 -78
- package/templates/social/data/migrations/002_add_image_url.sql +0 -1
- package/templates/social/data/migrations/003_auth.sql +0 -7
- package/templates/social/docs/architecture.md +0 -76
- package/templates/social/docs/components.md +0 -100
- package/templates/social/docs/data.md +0 -89
- package/templates/social/docs/pages.md +0 -96
- package/templates/social/docs/theming.md +0 -52
- package/templates/social/lib/media.ts +0 -130
- package/templates/social/lumenjs.auth.ts +0 -21
- package/templates/social/lumenjs.config.ts +0 -3
- package/templates/social/package.json +0 -5
- package/templates/social/pages/_layout.ts +0 -239
- package/templates/social/pages/apps/[id].ts +0 -173
- package/templates/social/pages/apps/index.ts +0 -116
- package/templates/social/pages/auth/login.ts +0 -92
- package/templates/social/pages/bookmarks.ts +0 -57
- package/templates/social/pages/explore.ts +0 -73
- package/templates/social/pages/index.ts +0 -351
- package/templates/social/pages/messages.ts +0 -298
- package/templates/social/pages/new.ts +0 -77
- package/templates/social/pages/notifications.ts +0 -73
- package/templates/social/pages/post/[id].ts +0 -124
- package/templates/social/pages/profile/[username].ts +0 -100
- package/templates/social/pages/settings/accessibility.ts +0 -153
- package/templates/social/pages/settings/account.ts +0 -260
- package/templates/social/pages/settings/help.ts +0 -141
- package/templates/social/pages/settings/language.ts +0 -103
- package/templates/social/pages/settings/privacy.ts +0 -183
- package/templates/social/pages/settings/security.ts +0 -133
- package/templates/social/pages/settings.ts +0 -185
|
@@ -2,14 +2,13 @@ import { build as viteBuild } from 'vite';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
export async function buildServer(opts) {
|
|
5
|
-
const { projectDir, serverDir, pageEntries, layoutEntries, apiEntries, hasAuthConfig, authConfigPath, shared } = opts;
|
|
5
|
+
const { projectDir, serverDir, pageEntries, layoutEntries, apiEntries, middlewareEntries, hasAuthConfig, authConfigPath, shared } = opts;
|
|
6
6
|
console.log('[LumenJS] Building server bundle...');
|
|
7
7
|
// Collect server entry points (pages with loaders + layouts with loaders + API routes)
|
|
8
8
|
const serverEntries = {};
|
|
9
|
+
// Include all pages in server build (enables SSR for .md endpoints)
|
|
9
10
|
for (const entry of pageEntries) {
|
|
10
|
-
|
|
11
|
-
serverEntries[`pages/${entry.name}`] = entry.filePath;
|
|
12
|
-
}
|
|
11
|
+
serverEntries[`pages/${entry.name}`] = entry.filePath;
|
|
13
12
|
}
|
|
14
13
|
for (const entry of layoutEntries) {
|
|
15
14
|
if (entry.hasLoader || entry.hasSubscribe) {
|
|
@@ -20,6 +19,10 @@ export async function buildServer(opts) {
|
|
|
20
19
|
for (const entry of apiEntries) {
|
|
21
20
|
serverEntries[`api/${entry.name}`] = entry.filePath;
|
|
22
21
|
}
|
|
22
|
+
for (const entry of middlewareEntries) {
|
|
23
|
+
const entryName = entry.dir ? `middleware/${entry.dir}/_middleware` : 'middleware/_middleware';
|
|
24
|
+
serverEntries[entryName] = entry.filePath;
|
|
25
|
+
}
|
|
23
26
|
if (hasAuthConfig) {
|
|
24
27
|
serverEntries['auth-config'] = authConfigPath;
|
|
25
28
|
}
|
|
@@ -77,6 +80,11 @@ export async function buildServer(opts) {
|
|
|
77
80
|
'worker_threads', 'cluster', 'dns', 'tls', 'assert', 'constants',
|
|
78
81
|
// Native addons — must not be bundled, loaded from node_modules at runtime
|
|
79
82
|
'better-sqlite3',
|
|
83
|
+
// AWS SDK — optional peer dep, keep as runtime import so app can provide it
|
|
84
|
+
'@aws-sdk/client-s3',
|
|
85
|
+
'@aws-sdk/s3-request-presigner',
|
|
86
|
+
/^@aws-sdk\//,
|
|
87
|
+
/^@smithy\//,
|
|
80
88
|
],
|
|
81
89
|
},
|
|
82
90
|
},
|
package/dist/build/build.js
CHANGED
|
@@ -2,11 +2,13 @@ import path from 'path';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import { getSharedViteConfig } from '../dev-server/server.js';
|
|
4
4
|
import { readProjectConfig } from '../dev-server/config.js';
|
|
5
|
-
import { filePathToTagName } from '../shared/utils.js';
|
|
6
|
-
import { scanPages, scanLayouts, scanApiRoutes, getLayoutDirsForPage } from './scan.js';
|
|
5
|
+
import { filePathToTagName, fileGetApiMethods } from '../shared/utils.js';
|
|
6
|
+
import { scanPages, scanLayouts, scanApiRoutes, scanMiddleware, getLayoutDirsForPage } from './scan.js';
|
|
7
7
|
import { buildClient } from './build-client.js';
|
|
8
8
|
import { buildServer } from './build-server.js';
|
|
9
9
|
import { prerenderPages } from './build-prerender.js';
|
|
10
|
+
import { generateMarkdownPages } from './build-markdown.js';
|
|
11
|
+
import { generateLlmsTxt } from '../llms/generate.js';
|
|
10
12
|
export async function buildProject(options) {
|
|
11
13
|
const { projectDir } = options;
|
|
12
14
|
const outDir = options.outDir || path.join(projectDir, '.lumenjs');
|
|
@@ -22,10 +24,11 @@ export async function buildProject(options) {
|
|
|
22
24
|
fs.mkdirSync(outDir, { recursive: true });
|
|
23
25
|
const { title, integrations, i18n: i18nConfig, prefetch: prefetchStrategy, prerender: globalPrerender } = readProjectConfig(projectDir);
|
|
24
26
|
const shared = getSharedViteConfig(projectDir, { mode: 'production', integrations });
|
|
25
|
-
// Scan pages, layouts,
|
|
27
|
+
// Scan pages, layouts, API routes, and middleware for the manifest
|
|
26
28
|
const pageEntries = scanPages(pagesDir);
|
|
27
29
|
const layoutEntries = scanLayouts(pagesDir);
|
|
28
30
|
const apiEntries = scanApiRoutes(apiDir);
|
|
31
|
+
const middlewareEntries = scanMiddleware(pagesDir);
|
|
29
32
|
// Check for auth config
|
|
30
33
|
const authConfigPath = path.join(projectDir, 'lumenjs.auth.ts');
|
|
31
34
|
const hasAuthConfig = fs.existsSync(authConfigPath);
|
|
@@ -52,6 +55,7 @@ export async function buildProject(options) {
|
|
|
52
55
|
pageEntries,
|
|
53
56
|
layoutEntries,
|
|
54
57
|
apiEntries,
|
|
58
|
+
middlewareEntries,
|
|
55
59
|
hasAuthConfig,
|
|
56
60
|
authConfigPath,
|
|
57
61
|
shared,
|
|
@@ -77,11 +81,12 @@ export async function buildProject(options) {
|
|
|
77
81
|
const relPath = path.relative(pagesDir, e.filePath).replace(/\\/g, '/');
|
|
78
82
|
return {
|
|
79
83
|
path: e.routePath,
|
|
80
|
-
module:
|
|
84
|
+
module: `pages/${e.name.replace(/\[(\w+)\]/g, '_$1_')}.js`,
|
|
81
85
|
hasLoader: e.hasLoader,
|
|
82
86
|
hasSubscribe: e.hasSubscribe,
|
|
83
87
|
tagName: filePathToTagName(relPath),
|
|
84
88
|
...(routeLayouts.length > 0 ? { layouts: routeLayouts } : {}),
|
|
89
|
+
...(e.hasSocket ? { hasSocket: true } : {}),
|
|
85
90
|
...(e.hasAuth ? { hasAuth: true } : {}),
|
|
86
91
|
...(e.hasMeta ? { hasMeta: true } : {}),
|
|
87
92
|
...(e.hasStandalone ? { hasStandalone: true } : {}),
|
|
@@ -90,7 +95,7 @@ export async function buildProject(options) {
|
|
|
90
95
|
}),
|
|
91
96
|
apiRoutes: apiEntries.map(e => ({
|
|
92
97
|
path: `/api/${e.routePath}`,
|
|
93
|
-
module: `api/${e.name}.js`,
|
|
98
|
+
module: `api/${e.name.replace(/\[(\w+)\]/g, '_$1_')}.js`,
|
|
94
99
|
hasLoader: false,
|
|
95
100
|
hasSubscribe: false,
|
|
96
101
|
})),
|
|
@@ -100,11 +105,39 @@ export async function buildProject(options) {
|
|
|
100
105
|
hasLoader: e.hasLoader,
|
|
101
106
|
hasSubscribe: e.hasSubscribe,
|
|
102
107
|
})),
|
|
108
|
+
...(middlewareEntries.length > 0 ? {
|
|
109
|
+
middlewares: middlewareEntries.map(e => ({
|
|
110
|
+
dir: e.dir,
|
|
111
|
+
module: e.dir ? `middleware/${e.dir}/_middleware.js` : 'middleware/_middleware.js',
|
|
112
|
+
})),
|
|
113
|
+
} : {}),
|
|
103
114
|
...(i18nConfig ? { i18n: i18nConfig } : {}),
|
|
104
115
|
...(hasAuthConfig ? { auth: { configModule: 'auth-config.js' } } : {}),
|
|
105
116
|
prefetch: prefetchStrategy,
|
|
106
117
|
};
|
|
107
118
|
fs.writeFileSync(path.join(outDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
119
|
+
// --- Generate llms.txt ---
|
|
120
|
+
const publicLlms = path.join(publicDir, 'llms.txt');
|
|
121
|
+
if (!fs.existsSync(publicLlms)) {
|
|
122
|
+
const llmsPages = pageEntries.map(e => ({
|
|
123
|
+
path: e.routePath,
|
|
124
|
+
hasLoader: e.hasLoader,
|
|
125
|
+
hasSubscribe: e.hasSubscribe,
|
|
126
|
+
}));
|
|
127
|
+
const llmsApiRoutes = apiEntries.map(e => ({
|
|
128
|
+
path: e.routePath,
|
|
129
|
+
methods: fileGetApiMethods(e.filePath),
|
|
130
|
+
})).filter(r => r.methods.length > 0);
|
|
131
|
+
const llmsContent = generateLlmsTxt({
|
|
132
|
+
title,
|
|
133
|
+
pages: llmsPages,
|
|
134
|
+
apiRoutes: llmsApiRoutes,
|
|
135
|
+
integrations,
|
|
136
|
+
i18n: i18nConfig ? { locales: i18nConfig.locales, defaultLocale: i18nConfig.defaultLocale } : undefined,
|
|
137
|
+
});
|
|
138
|
+
fs.writeFileSync(path.join(clientDir, 'llms.txt'), llmsContent);
|
|
139
|
+
console.log('[LumenJS] Generated llms.txt');
|
|
140
|
+
}
|
|
108
141
|
// --- Pre-render phase ---
|
|
109
142
|
await prerenderPages({
|
|
110
143
|
serverDir,
|
|
@@ -114,6 +147,14 @@ export async function buildProject(options) {
|
|
|
114
147
|
layoutEntries,
|
|
115
148
|
manifest,
|
|
116
149
|
});
|
|
150
|
+
// --- Generate .md files for each page (llms.txt per-page support) ---
|
|
151
|
+
await generateMarkdownPages({
|
|
152
|
+
serverDir,
|
|
153
|
+
clientDir,
|
|
154
|
+
pagesDir,
|
|
155
|
+
pageEntries,
|
|
156
|
+
manifest,
|
|
157
|
+
});
|
|
117
158
|
console.log('[LumenJS] Build complete.');
|
|
118
159
|
console.log(` Output: ${outDir}`);
|
|
119
160
|
console.log(` Client assets: ${clientDir}`);
|
package/dist/build/scan.d.ts
CHANGED
package/dist/build/scan.js
CHANGED
|
@@ -17,6 +17,7 @@ function analyzePageFile(filePath) {
|
|
|
17
17
|
return {
|
|
18
18
|
hasLoader: hasExportBefore(/export\s+(async\s+)?function\s+loader\s*\(/),
|
|
19
19
|
hasSubscribe: hasExportBefore(/export\s+(async\s+)?function\s+subscribe\s*\(/),
|
|
20
|
+
hasSocket: /export\s+(function|const)\s+socket[\s(=]/.test(content),
|
|
20
21
|
hasAuth: hasExportBefore(/export\s+const\s+auth\s*=/),
|
|
21
22
|
hasMeta: hasExportBefore(/export\s+(const\s+meta\s*=|(async\s+)?function\s+meta\s*\()/),
|
|
22
23
|
hasStandalone: hasExportBefore(/export\s+const\s+standalone\s*=/),
|
|
@@ -24,7 +25,7 @@ function analyzePageFile(filePath) {
|
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
catch {
|
|
27
|
-
return { hasLoader: false, hasSubscribe: false, hasAuth: false, hasMeta: false, hasStandalone: false, prerender: false };
|
|
28
|
+
return { hasLoader: false, hasSubscribe: false, hasSocket: false, hasAuth: false, hasMeta: false, hasStandalone: false, prerender: false };
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
export function scanPages(pagesDir) {
|
|
@@ -4,6 +4,8 @@ import { createGzip } from 'zlib';
|
|
|
4
4
|
import { pipeline } from 'stream';
|
|
5
5
|
export const MIME_TYPES = {
|
|
6
6
|
'.html': 'text/html; charset=utf-8',
|
|
7
|
+
'.md': 'text/markdown; charset=utf-8',
|
|
8
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
7
9
|
'.js': 'application/javascript; charset=utf-8',
|
|
8
10
|
'.mjs': 'application/javascript; charset=utf-8',
|
|
9
11
|
'.css': 'text/css; charset=utf-8',
|
|
@@ -21,7 +23,6 @@ export const MIME_TYPES = {
|
|
|
21
23
|
'.eot': 'application/vnd.ms-fontobject',
|
|
22
24
|
'.otf': 'font/otf',
|
|
23
25
|
'.map': 'application/json',
|
|
24
|
-
'.txt': 'text/plain; charset=utf-8',
|
|
25
26
|
'.xml': 'application/xml',
|
|
26
27
|
'.webmanifest': 'application/manifest+json',
|
|
27
28
|
};
|
package/dist/build/serve.js
CHANGED
|
@@ -22,6 +22,8 @@ import { createHealthCheckHandler } from '../shared/health.js';
|
|
|
22
22
|
import { createRequestIdMiddleware } from '../shared/request-id.js';
|
|
23
23
|
import { getRequestId } from '../shared/request-id.js';
|
|
24
24
|
import { setupGracefulShutdown } from '../shared/graceful-shutdown.js';
|
|
25
|
+
import { setupSocketIO } from '../shared/socket-io-setup.js';
|
|
26
|
+
import crypto from 'crypto';
|
|
25
27
|
export async function serveProject(options) {
|
|
26
28
|
const { projectDir } = options;
|
|
27
29
|
const port = options.port || 3000;
|
|
@@ -51,7 +53,9 @@ export async function serveProject(options) {
|
|
|
51
53
|
logger.fatal('No index.html found in build output.');
|
|
52
54
|
process.exit(1);
|
|
53
55
|
}
|
|
54
|
-
|
|
56
|
+
let indexHtmlShell = fs.readFileSync(indexHtmlPath, 'utf-8');
|
|
57
|
+
// Substitute env var placeholders (e.g. __UMAMI_WEBSITE_ID__)
|
|
58
|
+
indexHtmlShell = indexHtmlShell.replace(/__([A-Z0-9_]+)__/g, (_, key) => process.env[key] || '');
|
|
55
59
|
// Load bundled SSR runtime first — its install-global-dom-shim sets up
|
|
56
60
|
// the proper HTMLElement/window/document shims that @lit-labs/ssr needs.
|
|
57
61
|
// The lit-shared chunk handles missing HTMLElement via a fallback to its own shim,
|
|
@@ -195,14 +199,8 @@ export async function serveProject(options) {
|
|
|
195
199
|
if (res.writableEnded)
|
|
196
200
|
return;
|
|
197
201
|
}
|
|
198
|
-
//
|
|
199
|
-
if (
|
|
200
|
-
const handled = await handleAuthRoutes(authConfig, req, res, authDb);
|
|
201
|
-
if (handled)
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
// 0. Run user middleware chain
|
|
205
|
-
if (middlewareModules.size > 0 && !pathname.includes('.') && !pathname.startsWith('/__nk_')) {
|
|
202
|
+
// 0. Run user middleware chain (runs before auth routes so middleware can gate signup etc.)
|
|
203
|
+
if (middlewareModules.size > 0 && !pathname.includes('.') && (!pathname.startsWith('/__nk_') || pathname.startsWith('/__nk_auth/'))) {
|
|
206
204
|
const matching = getMiddlewareDirsForPathname(pathname, middlewareEntries);
|
|
207
205
|
const allMw = [];
|
|
208
206
|
for (const entry of matching) {
|
|
@@ -221,12 +219,120 @@ export async function serveProject(options) {
|
|
|
221
219
|
return;
|
|
222
220
|
}
|
|
223
221
|
}
|
|
224
|
-
// 1.
|
|
222
|
+
// 1. Auth routes (login, logout, me, signup, etc. — runs after user middleware so invite gate can intercept)
|
|
223
|
+
if (authConfig && pathname.startsWith('/__nk_auth/')) {
|
|
224
|
+
const handled = await handleAuthRoutes(authConfig, req, res, authDb);
|
|
225
|
+
if (handled)
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
// 2. API routes
|
|
225
229
|
if (pathname.startsWith('/api/')) {
|
|
226
230
|
await handleApiRoute(manifest, serverDir, pathname, queryString, method, req, res);
|
|
227
231
|
return;
|
|
228
232
|
}
|
|
229
|
-
//
|
|
233
|
+
// 2b. Communication file upload
|
|
234
|
+
if (pathname === '/__nk_comm/upload' && method === 'POST') {
|
|
235
|
+
const userId = req.nkAuth?.user?.sub;
|
|
236
|
+
if (!userId) {
|
|
237
|
+
res.statusCode = 401;
|
|
238
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const MAX_UPLOAD_SIZE = 10 * 1024 * 1024;
|
|
242
|
+
const chunks = [];
|
|
243
|
+
let uploadSize = 0;
|
|
244
|
+
let aborted = false;
|
|
245
|
+
req.on('data', (c) => {
|
|
246
|
+
uploadSize += c.length;
|
|
247
|
+
if (uploadSize > MAX_UPLOAD_SIZE) {
|
|
248
|
+
aborted = true;
|
|
249
|
+
req.destroy();
|
|
250
|
+
res.statusCode = 413;
|
|
251
|
+
res.end(JSON.stringify({ error: 'File too large' }));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
chunks.push(c);
|
|
255
|
+
});
|
|
256
|
+
req.on('end', async () => {
|
|
257
|
+
if (aborted)
|
|
258
|
+
return;
|
|
259
|
+
try {
|
|
260
|
+
const body = Buffer.concat(chunks);
|
|
261
|
+
const id = crypto.randomUUID();
|
|
262
|
+
const fileName = req.headers['x-filename'] || `file-${id}`;
|
|
263
|
+
const mimeType = req.headers['content-type'] || 'application/octet-stream';
|
|
264
|
+
const ext = fileName.includes('.') ? `.${fileName.split('.').pop()}` : '';
|
|
265
|
+
const key = `chat-uploads/${id}${ext}`;
|
|
266
|
+
// Use R2/S3 if configured, otherwise fall back to local disk
|
|
267
|
+
if (process.env.R2_BUCKET && process.env.R2_ENDPOINT) {
|
|
268
|
+
const { S3StorageAdapter } = await import('../storage/adapters/s3.js');
|
|
269
|
+
const s3 = new S3StorageAdapter({
|
|
270
|
+
bucket: process.env.R2_BUCKET,
|
|
271
|
+
region: 'auto',
|
|
272
|
+
accessKeyId: process.env.LUMENJS_S3_ACCESS_KEY || '',
|
|
273
|
+
secretAccessKey: process.env.LUMENJS_S3_SECRET_KEY || '',
|
|
274
|
+
endpoint: process.env.R2_ENDPOINT,
|
|
275
|
+
publicBaseUrl: process.env.R2_PUBLIC_URL,
|
|
276
|
+
});
|
|
277
|
+
const stored = await s3.put(body, { key, mimeType, fileName });
|
|
278
|
+
res.statusCode = 201;
|
|
279
|
+
res.setHeader('Content-Type', 'application/json');
|
|
280
|
+
res.end(JSON.stringify({ id, url: stored.url, size: body.length }));
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
// Local fallback
|
|
284
|
+
const uploadDir = path.join(projectDir, 'data', 'uploads');
|
|
285
|
+
if (!fs.existsSync(uploadDir))
|
|
286
|
+
fs.mkdirSync(uploadDir, { recursive: true });
|
|
287
|
+
fs.writeFileSync(path.join(uploadDir, `${id}.bin`), body);
|
|
288
|
+
fs.writeFileSync(path.join(uploadDir, `${id}.meta.json`), JSON.stringify({ id, filename: fileName, mimetype: mimeType, size: body.length }));
|
|
289
|
+
res.statusCode = 201;
|
|
290
|
+
res.setHeader('Content-Type', 'application/json');
|
|
291
|
+
res.end(JSON.stringify({ id, url: `/__nk_comm/files/${id}`, size: body.length }));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
logger.error('Upload failed', { error: err?.message });
|
|
296
|
+
res.statusCode = 500;
|
|
297
|
+
res.end(JSON.stringify({ error: 'Upload failed' }));
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
// Serve locally stored files (fallback when R2 is not configured)
|
|
303
|
+
if (pathname.startsWith('/__nk_comm/files/') && method === 'GET') {
|
|
304
|
+
const fileId = pathname.slice('/__nk_comm/files/'.length);
|
|
305
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(fileId)) {
|
|
306
|
+
res.statusCode = 400;
|
|
307
|
+
res.end('Invalid file ID');
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const uploadDir = path.join(projectDir, 'data', 'uploads');
|
|
311
|
+
const filePath = path.resolve(uploadDir, `${fileId}.bin`);
|
|
312
|
+
if (!filePath.startsWith(path.resolve(uploadDir))) {
|
|
313
|
+
res.statusCode = 400;
|
|
314
|
+
res.end('Invalid file ID');
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (!fs.existsSync(filePath)) {
|
|
318
|
+
res.statusCode = 404;
|
|
319
|
+
res.end('File not found');
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
let contentType = 'application/octet-stream';
|
|
323
|
+
try {
|
|
324
|
+
const meta = JSON.parse(fs.readFileSync(path.resolve(uploadDir, `${fileId}.meta.json`), 'utf-8'));
|
|
325
|
+
contentType = meta.mimetype || contentType;
|
|
326
|
+
}
|
|
327
|
+
catch { }
|
|
328
|
+
const stat = fs.statSync(filePath);
|
|
329
|
+
res.setHeader('Content-Type', contentType);
|
|
330
|
+
res.setHeader('Content-Length', stat.size);
|
|
331
|
+
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
|
332
|
+
fs.createReadStream(filePath).pipe(res);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
// 3. Static assets — try to serve from client dir (includes pre-generated .md files)
|
|
230
336
|
if (pathname.includes('.')) {
|
|
231
337
|
const served = serveStaticFile(clientDir, pathname, req, res);
|
|
232
338
|
if (served)
|
|
@@ -298,6 +404,20 @@ export async function serveProject(options) {
|
|
|
298
404
|
logger.request(req, res.statusCode, duration, { requestId: getRequestId(req) });
|
|
299
405
|
}
|
|
300
406
|
});
|
|
407
|
+
// Socket.IO setup (attach before listen so it shares the HTTP server)
|
|
408
|
+
const socketRoutes = manifest.routes
|
|
409
|
+
.filter(r => r.hasSocket && r.module)
|
|
410
|
+
.map(r => ({ path: r.path, hasSocket: true, filePath: path.join(serverDir, r.module) }));
|
|
411
|
+
if (socketRoutes.length > 0) {
|
|
412
|
+
setupSocketIO({
|
|
413
|
+
httpServer: server,
|
|
414
|
+
loadModule: (fp) => import(fp),
|
|
415
|
+
routes: socketRoutes,
|
|
416
|
+
projectDir,
|
|
417
|
+
}).catch((err) => {
|
|
418
|
+
logger.warn('Socket.IO setup failed', { error: err?.message });
|
|
419
|
+
});
|
|
420
|
+
}
|
|
301
421
|
// Graceful shutdown
|
|
302
422
|
setupGracefulShutdown(server, {
|
|
303
423
|
onShutdown: async () => {
|
|
@@ -12,6 +12,7 @@ export function readProjectConfig(projectDir) {
|
|
|
12
12
|
let prefetch = 'viewport';
|
|
13
13
|
let prerender;
|
|
14
14
|
let i18n;
|
|
15
|
+
let securityHeaders;
|
|
15
16
|
const configPath = path.join(projectDir, 'lumenjs.config.ts');
|
|
16
17
|
if (fs.existsSync(configPath)) {
|
|
17
18
|
try {
|
|
@@ -38,6 +39,22 @@ export function readProjectConfig(projectDir) {
|
|
|
38
39
|
if (prerenderMatch) {
|
|
39
40
|
prerender = prerenderMatch[1] === 'true';
|
|
40
41
|
}
|
|
42
|
+
const secHeadersMatch = configContent.match(/securityHeaders\s*:\s*\{([\s\S]*?)\}/);
|
|
43
|
+
if (secHeadersMatch) {
|
|
44
|
+
const block = secHeadersMatch[1];
|
|
45
|
+
const cspMatch = block.match(/contentSecurityPolicy\s*:\s*"([^"]+)"/)
|
|
46
|
+
|| block.match(/contentSecurityPolicy\s*:\s*'([^']+)'/)
|
|
47
|
+
|| block.match(/contentSecurityPolicy\s*:\s*`([^`]+)`/);
|
|
48
|
+
const ppMatch = block.match(/permissionsPolicy\s*:\s*'([^']+)'/)
|
|
49
|
+
|| block.match(/permissionsPolicy\s*:\s*"([^"]+)"/)
|
|
50
|
+
|| block.match(/permissionsPolicy\s*:\s*`([^`]+)`/);
|
|
51
|
+
if (cspMatch || ppMatch) {
|
|
52
|
+
securityHeaders = {
|
|
53
|
+
...(cspMatch ? { contentSecurityPolicy: cspMatch[1] } : {}),
|
|
54
|
+
...(ppMatch ? { permissionsPolicy: ppMatch[1] } : {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
41
58
|
const i18nMatch = configContent.match(/i18n\s*:\s*\{([\s\S]*?)\}/);
|
|
42
59
|
if (i18nMatch) {
|
|
43
60
|
const block = i18nMatch[1];
|
|
@@ -69,7 +86,7 @@ export function readProjectConfig(projectDir) {
|
|
|
69
86
|
}
|
|
70
87
|
catch { /* ignore */ }
|
|
71
88
|
}
|
|
72
|
-
return { title, integrations, prefetch, version, ...(i18n ? { i18n } : {}), ...(prerender ? { prerender } : {}) };
|
|
89
|
+
return { title, integrations, prefetch, version, ...(i18n ? { i18n } : {}), ...(prerender ? { prerender } : {}), ...(securityHeaders ? { securityHeaders } : {}) };
|
|
73
90
|
}
|
|
74
91
|
/**
|
|
75
92
|
* Reads the project title from lumenjs.config.ts (or returns default).
|
|
@@ -4,6 +4,9 @@ import { escapeHtml } from '../shared/utils.js';
|
|
|
4
4
|
* Includes the router, app shell, and optionally the editor bridge.
|
|
5
5
|
*/
|
|
6
6
|
export function generateIndexHtml(options) {
|
|
7
|
+
// Note: script src uses /@lumenjs/ (no base prefix) because Vite's
|
|
8
|
+
// transformIndexHtml already prepends config.base to absolute URLs.
|
|
9
|
+
// Including base here would double it when base != '/'.
|
|
7
10
|
const editorScript = options.editorMode
|
|
8
11
|
? `<script type="module" src="/@lumenjs/editor-bridge"></script>`
|
|
9
12
|
: '';
|
|
@@ -50,7 +53,7 @@ export function generateIndexHtml(options) {
|
|
|
50
53
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content" />
|
|
51
54
|
<title>${escapeHtml(options.title)}</title>
|
|
52
55
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
53
|
-
${options.integrations?.includes('
|
|
56
|
+
${options.integrations?.includes('tailwind') ? '<script type="module">import "/styles/tailwind.css";</script>' : ''}
|
|
54
57
|
${options.headContent || ''}
|
|
55
58
|
<style>
|
|
56
59
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
@@ -75,6 +75,7 @@ export function lumenLlmsPlugin(projectDir) {
|
|
|
75
75
|
integrations: config.integrations,
|
|
76
76
|
i18n: config.i18n ? { locales: config.i18n.locales, defaultLocale: config.i18n.defaultLocale } : undefined,
|
|
77
77
|
db: config.db,
|
|
78
|
+
baseUrl: '',
|
|
78
79
|
});
|
|
79
80
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
80
81
|
res.setHeader('Cache-Control', 'no-store');
|
|
@@ -13,15 +13,16 @@ import { Plugin } from 'vite';
|
|
|
13
13
|
* }
|
|
14
14
|
*
|
|
15
15
|
* export class PageItem extends LitElement {
|
|
16
|
-
* @property({ type: Object })
|
|
16
|
+
* @property({ type: Object }) item = null;
|
|
17
|
+
* @property({ type: Number }) timestamp = 0;
|
|
17
18
|
* render() {
|
|
18
|
-
* return html`<h1>${this.
|
|
19
|
+
* return html`<h1>${this.item?.name}</h1>`;
|
|
19
20
|
* }
|
|
20
21
|
* }
|
|
21
22
|
*
|
|
22
23
|
* The loader runs server-side via /__nk_loader/<page-path>
|
|
23
24
|
* Layout loaders run via /__nk_loader/__layout/?__dir=<dir>
|
|
24
|
-
* The router auto-fetches and
|
|
25
|
+
* The router auto-fetches and spreads each key as an individual property on the element.
|
|
25
26
|
*/
|
|
26
27
|
export declare function lumenLoadersPlugin(pagesDir: string): Plugin;
|
|
27
28
|
/**
|
|
@@ -16,15 +16,16 @@ import { installDomShims } from '../../shared/dom-shims.js';
|
|
|
16
16
|
* }
|
|
17
17
|
*
|
|
18
18
|
* export class PageItem extends LitElement {
|
|
19
|
-
* @property({ type: Object })
|
|
19
|
+
* @property({ type: Object }) item = null;
|
|
20
|
+
* @property({ type: Number }) timestamp = 0;
|
|
20
21
|
* render() {
|
|
21
|
-
* return html`<h1>${this.
|
|
22
|
+
* return html`<h1>${this.item?.name}</h1>`;
|
|
22
23
|
* }
|
|
23
24
|
* }
|
|
24
25
|
*
|
|
25
26
|
* The loader runs server-side via /__nk_loader/<page-path>
|
|
26
27
|
* Layout loaders run via /__nk_loader/__layout/?__dir=<dir>
|
|
27
|
-
* The router auto-fetches and
|
|
28
|
+
* The router auto-fetches and spreads each key as an individual property on the element.
|
|
28
29
|
*/
|
|
29
30
|
export function lumenLoadersPlugin(pagesDir) {
|
|
30
31
|
return {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { dirToLayoutTagName, fileHasLoader, fileHasSubscribe, fileHasAuth, fileHasMeta, fileHasStandalone, filePathToRoute, filePathToTagName } from '../../shared/utils.js';
|
|
3
|
+
import { dirToLayoutTagName, fileHasLoader, fileHasSubscribe, fileHasSocket, fileHasAuth, fileHasMeta, fileHasStandalone, filePathToRoute, filePathToTagName } from '../../shared/utils.js';
|
|
4
4
|
const VIRTUAL_MODULE_ID = 'virtual:lumenjs-routes';
|
|
5
5
|
const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID;
|
|
6
6
|
/**
|
|
@@ -105,6 +105,7 @@ export function lumenRoutesPlugin(pagesDir) {
|
|
|
105
105
|
.map(r => {
|
|
106
106
|
const hasLoader = fileHasLoader(r.componentPath);
|
|
107
107
|
const hasSubscribe = fileHasSubscribe(r.componentPath);
|
|
108
|
+
const hasSocketFlag = fileHasSocket(r.componentPath);
|
|
108
109
|
const hasAuth = fileHasAuth(r.componentPath);
|
|
109
110
|
const hasMeta = fileHasMeta(r.componentPath);
|
|
110
111
|
const isStandalone = fileHasStandalone(r.componentPath);
|
|
@@ -121,7 +122,7 @@ export function lumenRoutesPlugin(pagesDir) {
|
|
|
121
122
|
});
|
|
122
123
|
layoutsStr = `, layouts: [${items.join(', ')}]`;
|
|
123
124
|
}
|
|
124
|
-
return ` { path: ${JSON.stringify(r.path)}, tagName: ${JSON.stringify(r.tagName)}${hasLoader ? ', hasLoader: true' : ''}${hasSubscribe ? ', hasSubscribe: true' : ''}${hasMeta ? ', hasMeta: true' : ''}${hasAuth ? ', __nk_has_auth: true' : ''}, load: () => import('${componentPath}')${layoutsStr} }`;
|
|
125
|
+
return ` { path: ${JSON.stringify(r.path)}, tagName: ${JSON.stringify(r.tagName)}${hasLoader ? ', hasLoader: true' : ''}${hasSubscribe ? ', hasSubscribe: true' : ''}${hasSocketFlag ? ', hasSocket: true' : ''}${hasMeta ? ', hasMeta: true' : ''}${hasAuth ? ', __nk_has_auth: true' : ''}, load: () => import('${componentPath}')${layoutsStr} }`;
|
|
125
126
|
})
|
|
126
127
|
.join(',\n');
|
|
127
128
|
return `export const routes = [\n${routeArray}\n];\n`;
|
|
@@ -19,6 +19,7 @@ export function virtualModulesPlugin(runtimeDir, editorDir) {
|
|
|
19
19
|
'communication': 'communication.js',
|
|
20
20
|
'webrtc': 'webrtc.js',
|
|
21
21
|
'error-boundary': 'error-boundary.js',
|
|
22
|
+
'island': 'island.js',
|
|
22
23
|
'hydrate-support': '__virtual__',
|
|
23
24
|
};
|
|
24
25
|
// Modules resolved via resolve.alias instead of virtual module.
|
|
@@ -36,17 +37,27 @@ export function virtualModulesPlugin(runtimeDir, editorDir) {
|
|
|
36
37
|
'standalone-overlay-styles': 'standalone-overlay-styles.js',
|
|
37
38
|
'standalone-file-panel': 'standalone-file-panel.js',
|
|
38
39
|
'overlay-utils': 'overlay-utils.js',
|
|
40
|
+
'overlay-events': 'overlay-events.js',
|
|
41
|
+
'overlay-hmr': 'overlay-hmr.js',
|
|
42
|
+
'overlay-selection': 'overlay-selection.js',
|
|
39
43
|
'text-toolbar': 'text-toolbar.js',
|
|
44
|
+
'toolbar-styles': 'toolbar-styles.js',
|
|
40
45
|
'editor-toolbar': 'editor-toolbar.js',
|
|
41
46
|
'css-rules': 'css-rules.js',
|
|
42
47
|
'ast-modification': 'ast-modification.js',
|
|
43
48
|
'ast-service': 'ast-service.js',
|
|
44
49
|
'file-service': 'file-service.js',
|
|
50
|
+
'file-editor': 'file-editor.js',
|
|
51
|
+
'syntax-highlighter': 'syntax-highlighter.js',
|
|
45
52
|
'property-registry': 'property-registry.js',
|
|
46
53
|
'properties-panel': 'properties-panel.js',
|
|
54
|
+
'properties-panel-persist': 'properties-panel-persist.js',
|
|
55
|
+
'properties-panel-rows': 'properties-panel-rows.js',
|
|
56
|
+
'properties-panel-styles': 'properties-panel-styles.js',
|
|
47
57
|
'i18n-key-gen': 'i18n-key-gen.js',
|
|
48
58
|
'ai-chat-panel': 'ai-chat-panel.js',
|
|
49
59
|
'ai-project-panel': 'ai-project-panel.js',
|
|
60
|
+
'ai-markdown': 'ai-markdown.js',
|
|
50
61
|
};
|
|
51
62
|
function rewriteRelativeImports(code, modules) {
|
|
52
63
|
for (const name of Object.keys(modules)) {
|
|
@@ -54,14 +65,26 @@ export function virtualModulesPlugin(runtimeDir, editorDir) {
|
|
|
54
65
|
// Aliased modules use @lumenjs/name (resolved by Vite alias).
|
|
55
66
|
// Virtual modules use /@lumenjs/name (resolved by this plugin).
|
|
56
67
|
const prefix = aliasedModules.has(name) ? '@lumenjs' : '/@lumenjs';
|
|
57
|
-
|
|
68
|
+
const escaped = file.replace('.', '\\.');
|
|
69
|
+
// Rewrite `from './file.js'`
|
|
70
|
+
code = code.replace(new RegExp(`from\\s+['"]\\.\\/${escaped}['"]`, 'g'), `from '${prefix}/${name}'`);
|
|
71
|
+
// Rewrite side-effect `import './file.js'`
|
|
72
|
+
code = code.replace(new RegExp(`import\\s+['"]\\.\\/${escaped}['"]`, 'g'), `import '${prefix}/${name}'`);
|
|
58
73
|
}
|
|
59
74
|
return code;
|
|
60
75
|
}
|
|
76
|
+
let viteBase = '/';
|
|
61
77
|
return {
|
|
62
78
|
name: 'lumenjs-virtual-modules',
|
|
63
79
|
enforce: 'pre',
|
|
80
|
+
configResolved(config) {
|
|
81
|
+
viteBase = config.base || '/';
|
|
82
|
+
},
|
|
64
83
|
resolveId(id) {
|
|
84
|
+
// Strip Vite base prefix if present (e.g. /__app_dev/{id}/@lumenjs/foo → /@lumenjs/foo)
|
|
85
|
+
if (viteBase !== '/' && id.startsWith(viteBase)) {
|
|
86
|
+
id = '/' + id.slice(viteBase.length);
|
|
87
|
+
}
|
|
65
88
|
const match = id.match(/^\/@lumenjs\/(.+)$/);
|
|
66
89
|
if (!match)
|
|
67
90
|
return;
|
|
@@ -143,13 +166,18 @@ globalThis.litElementHydrateSupport = ({LitElement}) => {
|
|
|
143
166
|
try {
|
|
144
167
|
hydrate(value, this.renderRoot, this.renderOptions);
|
|
145
168
|
} catch (err) {
|
|
146
|
-
// Digest mismatch —
|
|
147
|
-
console.warn('[LumenJS] Hydration
|
|
169
|
+
// Digest mismatch — re-render fresh but avoid visible flash
|
|
170
|
+
console.warn('[LumenJS] Hydration mismatch for <' + this.localName + '>, falling back to CSR');
|
|
148
171
|
const root = this.renderRoot;
|
|
149
|
-
|
|
150
|
-
delete root._$litPart$;
|
|
151
|
-
// Re-adopt styles since clearing removed SSR <style> tags
|
|
172
|
+
// Preserve adopted styles so content is never unstyled
|
|
152
173
|
adoptElementStyles(this);
|
|
174
|
+
// Remove only non-style children to keep styles applied during re-render
|
|
175
|
+
const toRemove = [];
|
|
176
|
+
for (let c = root.firstChild; c; c = c.nextSibling) {
|
|
177
|
+
if (c.nodeName !== 'STYLE') toRemove.push(c);
|
|
178
|
+
}
|
|
179
|
+
toRemove.forEach(c => root.removeChild(c));
|
|
180
|
+
delete root._$litPart$;
|
|
153
181
|
render(value, root, this.renderOptions);
|
|
154
182
|
}
|
|
155
183
|
} else {
|