@nuraly/lumenjs 0.3.0 → 0.6.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 +32 -0
- package/dist/build/build-server.js +19 -0
- package/dist/build/scan.js +8 -2
- package/dist/build/serve-loaders.js +12 -2
- package/dist/build/serve-ssr.js +12 -3
- package/dist/communication/server.js +1 -0
- package/dist/communication/signaling.d.ts +2 -0
- package/dist/communication/signaling.js +41 -0
- package/dist/dev-server/plugins/vite-plugin-loaders.js +16 -2
- package/dist/dev-server/ssr-render.js +15 -3
- package/dist/runtime/webrtc.d.ts +8 -1
- package/dist/runtime/webrtc.js +49 -15
- package/dist/shared/socket-io-setup.js +17 -2
- package/dist/shared/utils.js +15 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
# LumenJS
|
|
9
9
|
|
|
10
|
+
> **Mirror**: This repository is a read-only mirror of the original private repository, automatically synced on push to main.
|
|
11
|
+
|
|
10
12
|
A full-stack web framework for [Lit](https://lit.dev/) web components. File-based routing, server loaders, real-time subscriptions (SSE), SSR with hydration, nested layouts, API routes, i18n, and a visual editor — all powered by Vite.
|
|
11
13
|
|
|
12
14
|
## Getting Started
|
|
@@ -88,6 +90,36 @@ export class BlogPost extends LitElement {
|
|
|
88
90
|
}
|
|
89
91
|
```
|
|
90
92
|
|
|
93
|
+
### Splitting large loaders
|
|
94
|
+
|
|
95
|
+
For folder routes (`pages/foo/index.ts`), you can move the loader into a co-located `_loader.ts` file. The framework discovers it automatically — no import or wrapper needed in the page file.
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
pages/
|
|
99
|
+
└── dashboard/
|
|
100
|
+
├── index.ts ← page component only
|
|
101
|
+
└── _loader.ts ← auto-discovered loader
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// pages/dashboard/_loader.ts
|
|
106
|
+
export async function loader({ user }) {
|
|
107
|
+
const stats = await db.getStats(user.id);
|
|
108
|
+
return { stats };
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// pages/dashboard/index.ts — no loader here at all
|
|
114
|
+
export class PageDashboard extends LitElement {
|
|
115
|
+
static properties = { stats: { type: Array } };
|
|
116
|
+
stats = [];
|
|
117
|
+
render() { ... }
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Both patterns work side by side — the inline loader always takes precedence. Only folder routes (`index.ts`) support `_loader.ts` discovery; flat pages (`about.ts`) keep the loader inline.
|
|
122
|
+
|
|
91
123
|
### Loader Context
|
|
92
124
|
|
|
93
125
|
| Property | Type | Description |
|
|
@@ -9,6 +9,25 @@ export async function buildServer(opts) {
|
|
|
9
9
|
// Include all pages in server build (enables SSR for .md endpoints)
|
|
10
10
|
for (const entry of pageEntries) {
|
|
11
11
|
serverEntries[`pages/${entry.name}`] = entry.filePath;
|
|
12
|
+
// Co-located _loader.ts / _socket.ts for folder index pages
|
|
13
|
+
if (path.basename(entry.filePath).replace(/\.(ts|js)$/, '') === 'index') {
|
|
14
|
+
const dir = path.dirname(entry.filePath);
|
|
15
|
+
const entryDir = path.dirname(entry.name);
|
|
16
|
+
for (const ext of ['.ts', '.js']) {
|
|
17
|
+
const loaderFile = path.join(dir, `_loader${ext}`);
|
|
18
|
+
if (fs.existsSync(loaderFile)) {
|
|
19
|
+
serverEntries[`pages/${entryDir}/_loader`] = loaderFile;
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
for (const ext of ['.ts', '.js']) {
|
|
24
|
+
const socketFile = path.join(dir, `_socket${ext}`);
|
|
25
|
+
if (fs.existsSync(socketFile)) {
|
|
26
|
+
serverEntries[`pages/${entryDir}/_socket`] = socketFile;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
12
31
|
}
|
|
13
32
|
for (const entry of layoutEntries) {
|
|
14
33
|
if (entry.hasLoader || entry.hasSubscribe) {
|
package/dist/build/scan.js
CHANGED
|
@@ -14,10 +14,16 @@ function analyzePageFile(filePath) {
|
|
|
14
14
|
return false;
|
|
15
15
|
return true;
|
|
16
16
|
};
|
|
17
|
+
const isIndex = path.basename(filePath).replace(/\.(ts|js)$/, '') === 'index';
|
|
18
|
+
const dir = path.dirname(filePath);
|
|
19
|
+
const hasColocatedLoader = isIndex &&
|
|
20
|
+
(fs.existsSync(path.join(dir, '_loader.ts')) || fs.existsSync(path.join(dir, '_loader.js')));
|
|
21
|
+
const hasColocatedSocket = isIndex &&
|
|
22
|
+
(fs.existsSync(path.join(dir, '_socket.ts')) || fs.existsSync(path.join(dir, '_socket.js')));
|
|
17
23
|
return {
|
|
18
|
-
hasLoader: hasExportBefore(/export\s+(async\s+)?function\s+loader\s*\(/),
|
|
24
|
+
hasLoader: hasExportBefore(/export\s+(async\s+)?function\s+loader\s*\(/) || hasColocatedLoader,
|
|
19
25
|
hasSubscribe: hasExportBefore(/export\s+(async\s+)?function\s+subscribe\s*\(/),
|
|
20
|
-
hasSocket: /export\s+(function|const)\s+socket[\s(=]/.test(content),
|
|
26
|
+
hasSocket: /export\s+(function|const)\s+socket[\s(=]/.test(content) || hasColocatedSocket,
|
|
21
27
|
hasAuth: hasExportBefore(/export\s+const\s+auth\s*=/),
|
|
22
28
|
hasMeta: hasExportBefore(/export\s+(const\s+meta\s*=|(async\s+)?function\s+meta\s*\()/),
|
|
23
29
|
hasStandalone: hasExportBefore(/export\s+const\s+standalone\s*=/),
|
|
@@ -207,14 +207,24 @@ export async function handleLoaderRequest(manifest, serverDir, pagesDir, pathnam
|
|
|
207
207
|
}
|
|
208
208
|
try {
|
|
209
209
|
const mod = await import(modulePath);
|
|
210
|
-
|
|
210
|
+
let loaderFn = mod.loader && typeof mod.loader === 'function' ? mod.loader : null;
|
|
211
|
+
// Fallback: co-located _loader.js for folder index pages
|
|
212
|
+
if (!loaderFn && path.basename(modulePath, '.js') === 'index') {
|
|
213
|
+
const colocated = path.join(path.dirname(modulePath), '_loader.js');
|
|
214
|
+
if (fs.existsSync(colocated)) {
|
|
215
|
+
const loaderMod = await import(colocated);
|
|
216
|
+
if (loaderMod.loader && typeof loaderMod.loader === 'function')
|
|
217
|
+
loaderFn = loaderMod.loader;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (!loaderFn) {
|
|
211
221
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
212
222
|
res.end(JSON.stringify({ __nk_no_loader: true }));
|
|
213
223
|
return;
|
|
214
224
|
}
|
|
215
225
|
const locale = query.__locale;
|
|
216
226
|
delete query.__locale;
|
|
217
|
-
const result = await
|
|
227
|
+
const result = await loaderFn({ params: matched.params, query, url: pagePath, headers, locale, user: user ?? null });
|
|
218
228
|
if (isRedirectResponse(result)) {
|
|
219
229
|
res.writeHead(result.status || 302, { Location: result.location });
|
|
220
230
|
res.end();
|
package/dist/build/serve-ssr.js
CHANGED
|
@@ -18,10 +18,19 @@ export async function handlePageRoute(manifest, serverDir, pagesDir, pathname, q
|
|
|
18
18
|
if (fs.existsSync(modulePath)) {
|
|
19
19
|
try {
|
|
20
20
|
const mod = await import(modulePath);
|
|
21
|
-
// Run loader
|
|
21
|
+
// Run loader (inline or co-located _loader.js)
|
|
22
22
|
let loaderData = undefined;
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
let loaderFn = mod.loader && typeof mod.loader === 'function' ? mod.loader : null;
|
|
24
|
+
if (!loaderFn && path.basename(modulePath, '.js') === 'index') {
|
|
25
|
+
const colocated = path.join(path.dirname(modulePath), '_loader.js');
|
|
26
|
+
if (fs.existsSync(colocated)) {
|
|
27
|
+
const loaderMod = await import(colocated);
|
|
28
|
+
if (loaderMod.loader && typeof loaderMod.loader === 'function')
|
|
29
|
+
loaderFn = loaderMod.loader;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (loaderFn) {
|
|
33
|
+
loaderData = await loaderFn({ params: matched.params, query: {}, url: pathname, headers: req.headers, user: req.nkAuth?.user ?? null });
|
|
25
34
|
if (isRedirectResponse(loaderData)) {
|
|
26
35
|
res.writeHead(loaderData.status || 302, { Location: loaderData.location });
|
|
27
36
|
res.end();
|
|
@@ -8,6 +8,8 @@ export interface SignalingContext {
|
|
|
8
8
|
emitToSocket: (socketId: string, data: any) => void;
|
|
9
9
|
/** Broadcast to all sockets in a room */
|
|
10
10
|
broadcastAll: (room: string, data: any) => void;
|
|
11
|
+
/** Optional database for persisting call logs */
|
|
12
|
+
db?: any;
|
|
11
13
|
}
|
|
12
14
|
export declare function handleCallInitiate(ctx: SignalingContext, data: {
|
|
13
15
|
conversationId: string;
|
|
@@ -130,6 +130,47 @@ export function handleCallHangup(ctx, data) {
|
|
|
130
130
|
data: { callId: data.callId, state: 'ended', endReason: data.reason },
|
|
131
131
|
});
|
|
132
132
|
}
|
|
133
|
+
// Persist call log as a message in the conversation
|
|
134
|
+
if (ctx.db && call.conversationId) {
|
|
135
|
+
try {
|
|
136
|
+
const callStatus = data.reason === 'rejected' ? 'declined'
|
|
137
|
+
: data.reason === 'missed' ? 'missed'
|
|
138
|
+
: 'completed';
|
|
139
|
+
const attachment = JSON.stringify({
|
|
140
|
+
callType: call.type || 'audio',
|
|
141
|
+
callStatus,
|
|
142
|
+
duration: data.duration || null,
|
|
143
|
+
});
|
|
144
|
+
const msgId = `call-${data.callId}`;
|
|
145
|
+
const isPg = !!ctx.db.isPg;
|
|
146
|
+
if (isPg) {
|
|
147
|
+
ctx.db.run(`INSERT INTO messages (id, conversation_id, sender_id, content, type, attachment, created_at)
|
|
148
|
+
VALUES ($1, $2, $3, $4, $5, $6, NOW()) ON CONFLICT (id) DO NOTHING`, msgId, call.conversationId, call.callerId, '', 'call', attachment);
|
|
149
|
+
ctx.db.run(`UPDATE conversations SET updated_at = NOW() WHERE id = $1`, call.conversationId);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
ctx.db.run(`INSERT OR IGNORE INTO messages (id, conversation_id, sender_id, content, type, attachment, created_at)
|
|
153
|
+
VALUES (?, ?, ?, '', 'call', ?, datetime('now'))`, msgId, call.conversationId, call.callerId, attachment);
|
|
154
|
+
ctx.db.run(`UPDATE conversations SET updated_at = datetime('now') WHERE id = ?`, call.conversationId);
|
|
155
|
+
}
|
|
156
|
+
// Broadcast call message to all participants
|
|
157
|
+
const callMsg = {
|
|
158
|
+
id: msgId,
|
|
159
|
+
conversationId: call.conversationId,
|
|
160
|
+
senderId: call.callerId,
|
|
161
|
+
content: '',
|
|
162
|
+
type: 'call',
|
|
163
|
+
attachment: { callType: call.type || 'audio', callStatus, duration: data.duration || null },
|
|
164
|
+
createdAt: new Date().toISOString(),
|
|
165
|
+
};
|
|
166
|
+
for (const uid of allUsers) {
|
|
167
|
+
emitToUser(ctx, uid, { event: 'message:new', data: callMsg });
|
|
168
|
+
}
|
|
169
|
+
// Also emit to the caller
|
|
170
|
+
emitToUser(ctx, ctx.userId, { event: 'message:new', data: callMsg });
|
|
171
|
+
}
|
|
172
|
+
catch { }
|
|
173
|
+
}
|
|
133
174
|
ctx.store.removeCall(data.callId);
|
|
134
175
|
}
|
|
135
176
|
}
|
|
@@ -159,7 +159,21 @@ export function lumenLoadersPlugin(pagesDir) {
|
|
|
159
159
|
// Provide minimal DOM shims for SSR so Lit class definitions don't crash
|
|
160
160
|
installDomShims();
|
|
161
161
|
const mod = await server.ssrLoadModule(filePath);
|
|
162
|
-
|
|
162
|
+
let loaderFn = mod.loader && typeof mod.loader === 'function' ? mod.loader : null;
|
|
163
|
+
// Fallback: co-located _loader.ts for folder index pages
|
|
164
|
+
if (!loaderFn && path.basename(filePath).replace(/\.(ts|js)$/, '') === 'index') {
|
|
165
|
+
for (const ext of ['.ts', '.js']) {
|
|
166
|
+
const colocated = path.join(path.dirname(filePath), `_loader${ext}`);
|
|
167
|
+
if (fs.existsSync(colocated)) {
|
|
168
|
+
const loaderMod = await server.ssrLoadModule(colocated);
|
|
169
|
+
if (loaderMod.loader && typeof loaderMod.loader === 'function') {
|
|
170
|
+
loaderFn = loaderMod.loader;
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (!loaderFn) {
|
|
163
177
|
// No loader — return empty data
|
|
164
178
|
res.statusCode = 200;
|
|
165
179
|
res.setHeader('Content-Type', 'application/json');
|
|
@@ -205,7 +219,7 @@ export function lumenLoadersPlugin(pagesDir) {
|
|
|
205
219
|
}
|
|
206
220
|
catch { }
|
|
207
221
|
}
|
|
208
|
-
const result = await
|
|
222
|
+
const result = await loaderFn({ params, query, url: pagePath, headers: req.headers, locale, user });
|
|
209
223
|
if (isRedirectResponse(result)) {
|
|
210
224
|
res.statusCode = result.status || 302;
|
|
211
225
|
res.setHeader('Location', result.location);
|
|
@@ -44,10 +44,22 @@ export async function ssrRenderPage(server, pagesDir, pathname, headers, locale,
|
|
|
44
44
|
const mod = await server.ssrLoadModule(pageModuleUrl);
|
|
45
45
|
if (registry)
|
|
46
46
|
registry.__nk_bypass_get = false;
|
|
47
|
-
// Run loader if present
|
|
47
|
+
// Run loader if present (inline or co-located _loader.ts)
|
|
48
48
|
let loaderData = undefined;
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
let loaderFn = mod.loader && typeof mod.loader === 'function' ? mod.loader : null;
|
|
50
|
+
if (!loaderFn && path.basename(filePath).replace(/\.(ts|js)$/, '') === 'index') {
|
|
51
|
+
for (const ext of ['.ts', '.js']) {
|
|
52
|
+
const colocated = path.join(path.dirname(filePath), `_loader${ext}`);
|
|
53
|
+
if (fs.existsSync(colocated)) {
|
|
54
|
+
const loaderMod = await server.ssrLoadModule('/' + path.relative(path.resolve(pagesDir, '..'), colocated).replace(/\\/g, '/'));
|
|
55
|
+
if (loaderMod.loader && typeof loaderMod.loader === 'function')
|
|
56
|
+
loaderFn = loaderMod.loader;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (loaderFn) {
|
|
62
|
+
loaderData = await loaderFn({ params, query: {}, url: pathname, headers: headers || {}, locale, user: user ?? null });
|
|
51
63
|
if (loaderData && typeof loaderData === 'object' && loaderData.__nk_redirect) {
|
|
52
64
|
return { html: '', loaderData: null, redirect: { location: loaderData.location, status: loaderData.status || 302 } };
|
|
53
65
|
}
|
package/dist/runtime/webrtc.d.ts
CHANGED
|
@@ -66,7 +66,14 @@ export declare class GroupWebRTCManager {
|
|
|
66
66
|
getRemoteStream(userId: string): MediaStream | null;
|
|
67
67
|
getRemoteStreams(): Map<string, MediaStream>;
|
|
68
68
|
/** Acquire local media — call once before adding peers */
|
|
69
|
-
startLocalMedia(video?: boolean, audio?: boolean
|
|
69
|
+
startLocalMedia(video?: boolean, audio?: boolean, deviceIds?: {
|
|
70
|
+
audioInputId?: string | null;
|
|
71
|
+
videoInputId?: string | null;
|
|
72
|
+
}): Promise<MediaStream>;
|
|
73
|
+
/** Replace the audio track on all peer connections (for switching mic mid-call) */
|
|
74
|
+
replaceAudioTrack(newTrack: MediaStreamTrack): Promise<void>;
|
|
75
|
+
/** Replace the video track on all peer connections (for switching camera mid-call) */
|
|
76
|
+
replaceVideoTrack(newTrack: MediaStreamTrack): Promise<void>;
|
|
70
77
|
/** Create a peer connection for a remote user and optionally create an offer */
|
|
71
78
|
addPeer(userId: string, isCaller: boolean): Promise<string | void>;
|
|
72
79
|
/** Remove and close a peer connection */
|
package/dist/runtime/webrtc.js
CHANGED
|
@@ -54,16 +54,11 @@ export class WebRTCManager {
|
|
|
54
54
|
throw err;
|
|
55
55
|
}
|
|
56
56
|
try {
|
|
57
|
-
//
|
|
58
|
-
// For audio-only calls the
|
|
59
|
-
//
|
|
60
|
-
this._localStream = await navigator.mediaDevices.getUserMedia({ video
|
|
57
|
+
// Only request video when the call type is video.
|
|
58
|
+
// For audio-only calls, skip the camera entirely to avoid the permission
|
|
59
|
+
// prompt and camera LED. Screen sharing can add a video track later.
|
|
60
|
+
this._localStream = await navigator.mediaDevices.getUserMedia({ video, audio });
|
|
61
61
|
this._callbacks.onLocalStream(this._localStream);
|
|
62
|
-
if (!video) {
|
|
63
|
-
for (const vt of this._localStream.getVideoTracks()) {
|
|
64
|
-
vt.enabled = false;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
62
|
for (const track of this._localStream.getTracks()) {
|
|
68
63
|
this._pc?.addTrack(track, this._localStream);
|
|
69
64
|
}
|
|
@@ -232,19 +227,24 @@ export class GroupWebRTCManager {
|
|
|
232
227
|
return result;
|
|
233
228
|
}
|
|
234
229
|
/** Acquire local media — call once before adding peers */
|
|
235
|
-
async startLocalMedia(video = true, audio = true) {
|
|
230
|
+
async startLocalMedia(video = true, audio = true, deviceIds) {
|
|
236
231
|
if (!navigator.mediaDevices?.getUserMedia) {
|
|
237
232
|
const err = new Error('Media devices unavailable — HTTPS is required for calls');
|
|
238
233
|
this._callbacks.onError(err);
|
|
239
234
|
throw err;
|
|
240
235
|
}
|
|
241
236
|
try {
|
|
242
|
-
|
|
237
|
+
const audioConstraint = audio
|
|
238
|
+
? (deviceIds?.audioInputId ? { deviceId: { exact: deviceIds.audioInputId } } : true)
|
|
239
|
+
: false;
|
|
240
|
+
const videoConstraint = video
|
|
241
|
+
? (deviceIds?.videoInputId ? { deviceId: { exact: deviceIds.videoInputId } } : true)
|
|
242
|
+
: false;
|
|
243
|
+
this._localStream = await navigator.mediaDevices.getUserMedia({
|
|
244
|
+
audio: audioConstraint,
|
|
245
|
+
video: videoConstraint,
|
|
246
|
+
});
|
|
243
247
|
this._callbacks.onLocalStream(this._localStream);
|
|
244
|
-
if (!video) {
|
|
245
|
-
for (const vt of this._localStream.getVideoTracks())
|
|
246
|
-
vt.enabled = false;
|
|
247
|
-
}
|
|
248
248
|
return this._localStream;
|
|
249
249
|
}
|
|
250
250
|
catch (err) {
|
|
@@ -252,6 +252,40 @@ export class GroupWebRTCManager {
|
|
|
252
252
|
throw err;
|
|
253
253
|
}
|
|
254
254
|
}
|
|
255
|
+
/** Replace the audio track on all peer connections (for switching mic mid-call) */
|
|
256
|
+
async replaceAudioTrack(newTrack) {
|
|
257
|
+
// Replace in local stream
|
|
258
|
+
if (this._localStream) {
|
|
259
|
+
const old = this._localStream.getAudioTracks()[0];
|
|
260
|
+
if (old) {
|
|
261
|
+
this._localStream.removeTrack(old);
|
|
262
|
+
old.stop();
|
|
263
|
+
}
|
|
264
|
+
this._localStream.addTrack(newTrack);
|
|
265
|
+
}
|
|
266
|
+
// Replace on all peer connections
|
|
267
|
+
for (const [, entry] of this._peers) {
|
|
268
|
+
const sender = entry.pc.getSenders().find(s => s.track?.kind === 'audio');
|
|
269
|
+
if (sender)
|
|
270
|
+
await sender.replaceTrack(newTrack);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/** Replace the video track on all peer connections (for switching camera mid-call) */
|
|
274
|
+
async replaceVideoTrack(newTrack) {
|
|
275
|
+
if (this._localStream) {
|
|
276
|
+
const old = this._localStream.getVideoTracks()[0];
|
|
277
|
+
if (old) {
|
|
278
|
+
this._localStream.removeTrack(old);
|
|
279
|
+
old.stop();
|
|
280
|
+
}
|
|
281
|
+
this._localStream.addTrack(newTrack);
|
|
282
|
+
}
|
|
283
|
+
for (const [, entry] of this._peers) {
|
|
284
|
+
const sender = entry.pc.getSenders().find(s => s.track?.kind === 'video');
|
|
285
|
+
if (sender)
|
|
286
|
+
await sender.replaceTrack(newTrack);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
255
289
|
/** Create a peer connection for a remote user and optionally create an offer */
|
|
256
290
|
async addPeer(userId, isCaller) {
|
|
257
291
|
if (this._peers.has(userId))
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createRequire } from 'module';
|
|
2
2
|
import { pathToFileURL } from 'url';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
3
5
|
export async function setupSocketIO(options) {
|
|
4
6
|
// Resolve socket.io from the project's node_modules (not from lumenjs's own node_modules)
|
|
5
7
|
let SocketIOServer;
|
|
@@ -23,7 +25,20 @@ export async function setupSocketIO(options) {
|
|
|
23
25
|
io.of(ns).on('connection', async (socket) => {
|
|
24
26
|
try {
|
|
25
27
|
const mod = await options.loadModule(route.filePath);
|
|
26
|
-
|
|
28
|
+
let socketFn = mod?.socket;
|
|
29
|
+
// Co-located _socket.ts fallback (folder route convention)
|
|
30
|
+
if (!socketFn && path.basename(route.filePath).replace(/\.(ts|js)$/, '') === 'index') {
|
|
31
|
+
const dir = path.dirname(route.filePath);
|
|
32
|
+
for (const ext of ['.ts', '.js']) {
|
|
33
|
+
const colocated = path.join(dir, `_socket${ext}`);
|
|
34
|
+
if (fs.existsSync(colocated)) {
|
|
35
|
+
const socketMod = await options.loadModule(colocated);
|
|
36
|
+
socketFn = socketMod?.socket;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!socketFn)
|
|
27
42
|
return;
|
|
28
43
|
const push = (data) => socket.emit('nk:data', data);
|
|
29
44
|
const on = (event, handler) => {
|
|
@@ -38,7 +53,7 @@ export async function setupSocketIO(options) {
|
|
|
38
53
|
const params = socket.handshake.query.__params
|
|
39
54
|
? JSON.parse(socket.handshake.query.__params) : {};
|
|
40
55
|
const locale = socket.handshake.query.__locale;
|
|
41
|
-
const cleanup =
|
|
56
|
+
const cleanup = socketFn({ on, push, room, params, headers: socket.handshake.headers, locale, socket });
|
|
42
57
|
socket.on('disconnect', () => { if (typeof cleanup === 'function')
|
|
43
58
|
cleanup(); });
|
|
44
59
|
}
|
package/dist/shared/utils.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
2
3
|
/**
|
|
3
4
|
* Strip the outer Lit SSR template markers from rendered HTML.
|
|
4
5
|
* Lit SSR wraps every template render in <!--lit-part HASH-->...<!--/lit-part-->.
|
|
@@ -158,6 +159,13 @@ export function fileHasPrerender(filePath) {
|
|
|
158
159
|
*/
|
|
159
160
|
export function fileHasLoader(filePath) {
|
|
160
161
|
try {
|
|
162
|
+
// Check for co-located _loader.ts (folder route convention: index.ts + _loader.ts)
|
|
163
|
+
if (path.basename(filePath).replace(/\.(ts|js)$/, '') === 'index') {
|
|
164
|
+
const dir = path.dirname(filePath);
|
|
165
|
+
if (fs.existsSync(path.join(dir, '_loader.ts')) || fs.existsSync(path.join(dir, '_loader.js'))) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
161
169
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
162
170
|
return hasTopLevelExport(content, 'loader');
|
|
163
171
|
}
|
|
@@ -220,6 +228,13 @@ export function fileHasMeta(filePath) {
|
|
|
220
228
|
*/
|
|
221
229
|
export function fileHasSocket(filePath) {
|
|
222
230
|
try {
|
|
231
|
+
// Check for co-located _socket.ts (folder route convention: index.ts + _socket.ts)
|
|
232
|
+
if (path.basename(filePath).replace(/\.(ts|js)$/, '') === 'index') {
|
|
233
|
+
const dir = path.dirname(filePath);
|
|
234
|
+
if (fs.existsSync(path.join(dir, '_socket.ts')) || fs.existsSync(path.join(dir, '_socket.js'))) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
223
238
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
224
239
|
return /export\s+(function|const)\s+socket[\s(=]/.test(content);
|
|
225
240
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuraly/lumenjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Full-stack Lit web component framework with file-based routing, server loaders, SSR, and API routes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cli.js",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"license": "MIT",
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@lit-labs/ssr": "^3.2.0",
|
|
46
|
-
"glob": "^10.
|
|
46
|
+
"glob": "^10.2.1",
|
|
47
47
|
"lit": "^3.1.0",
|
|
48
48
|
"vite": "^5.4.0"
|
|
49
49
|
},
|