@nuraly/lumenjs 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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-server.d.ts +2 -1
- package/dist/build/build-server.js +10 -1
- package/dist/build/build.js +13 -4
- package/dist/build/scan.d.ts +1 -0
- package/dist/build/scan.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-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-chat-panel.js +27 -1
- package/dist/editor/editor-bridge.js +2 -1
- package/dist/editor/overlay-hmr.js +2 -1
- package/dist/runtime/app-shell.d.ts +1 -1
- package/dist/runtime/app-shell.js +1 -0
- 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 +49 -1
- 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/types.d.ts +1 -0
- package/dist/storage/adapters/s3.js +6 -3
- package/package.json +33 -7
- 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
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
|
|
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; }
|
|
@@ -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 {
|