@nuraly/lumenjs 0.2.0 → 0.5.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 +80 -7
- package/dist/build/build-markdown.d.ts +15 -0
- package/dist/build/build-markdown.js +90 -0
- package/dist/build/build-server.js +13 -2
- package/dist/build/build.js +34 -2
- package/dist/build/scan.js +4 -1
- package/dist/build/serve-loaders.js +12 -2
- package/dist/build/serve-ssr.js +12 -3
- package/dist/build/serve-static.js +2 -1
- package/dist/build/serve.js +1 -1
- 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-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 +20 -5
- package/dist/dev-server/ssr-render.js +15 -3
- package/dist/editor/ai/types.d.ts +1 -1
- package/dist/editor/ai/types.js +2 -2
- package/dist/llms/generate.d.ts +15 -1
- package/dist/llms/generate.js +54 -44
- package/dist/runtime/communication.d.ts +65 -36
- package/dist/runtime/communication.js +117 -57
- package/dist/runtime/router.js +3 -3
- package/dist/runtime/webrtc.d.ts +8 -1
- package/dist/runtime/webrtc.js +49 -15
- package/dist/shared/html-to-markdown.d.ts +6 -0
- package/dist/shared/html-to-markdown.js +73 -0
- package/dist/shared/utils.js +8 -0
- package/package.json +2 -2
- 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/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))
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert simple HTML to markdown.
|
|
3
|
+
* Handles the subset of HTML that Lit SSR produces for LumenJS pages.
|
|
4
|
+
* Not a full HTML parser — intentionally minimal.
|
|
5
|
+
*/
|
|
6
|
+
export function htmlToMarkdown(html) {
|
|
7
|
+
let md = html;
|
|
8
|
+
// Extract content from declarative shadow DOM (<template shadowroot="open">...</template>)
|
|
9
|
+
md = md.replace(/<template\s+shadowroot(?:mode)?="open"[^>]*>([\s\S]*?)<\/template>/gi, '$1');
|
|
10
|
+
// Remove script/style tags and their content
|
|
11
|
+
md = md.replace(/<script[\s\S]*?<\/script>/gi, '');
|
|
12
|
+
md = md.replace(/<style[\s\S]*?<\/style>/gi, '');
|
|
13
|
+
md = md.replace(/<template[\s\S]*?<\/template>/gi, '');
|
|
14
|
+
// Remove Lit SSR markers (<!--lit-part-->, <!--/lit-part-->, etc.)
|
|
15
|
+
md = md.replace(/<!--[\s\S]*?-->/g, '');
|
|
16
|
+
// Headings
|
|
17
|
+
md = md.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, (_, c) => `# ${strip(c)}\n\n`);
|
|
18
|
+
md = md.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, (_, c) => `## ${strip(c)}\n\n`);
|
|
19
|
+
md = md.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, (_, c) => `### ${strip(c)}\n\n`);
|
|
20
|
+
md = md.replace(/<h4[^>]*>([\s\S]*?)<\/h4>/gi, (_, c) => `#### ${strip(c)}\n\n`);
|
|
21
|
+
// Links
|
|
22
|
+
md = md.replace(/<a[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, (_, href, text) => `[${strip(text)}](${href})`);
|
|
23
|
+
// Bold / italic
|
|
24
|
+
md = md.replace(/<(strong|b)[^>]*>([\s\S]*?)<\/\1>/gi, (_, __, c) => `**${strip(c)}**`);
|
|
25
|
+
md = md.replace(/<(em|i)[^>]*>([\s\S]*?)<\/\1>/gi, (_, __, c) => `*${strip(c)}*`);
|
|
26
|
+
// Inline code
|
|
27
|
+
md = md.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, (_, c) => `\`${strip(c)}\``);
|
|
28
|
+
// Code blocks (pre > code or pre alone)
|
|
29
|
+
md = md.replace(/<pre[^>]*>\s*<code[^>]*>([\s\S]*?)<\/code>\s*<\/pre>/gi, (_, c) => `\n\`\`\`\n${decodeEntities(strip(c))}\n\`\`\`\n\n`);
|
|
30
|
+
md = md.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, (_, c) => `\n\`\`\`\n${decodeEntities(strip(c))}\n\`\`\`\n\n`);
|
|
31
|
+
// List items
|
|
32
|
+
md = md.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_, c) => `- ${strip(c).trim()}\n`);
|
|
33
|
+
// Table → simple text rows
|
|
34
|
+
md = md.replace(/<tr[^>]*>([\s\S]*?)<\/tr>/gi, (_, c) => {
|
|
35
|
+
const cells = [...c.matchAll(/<t[hd][^>]*>([\s\S]*?)<\/t[hd]>/gi)].map((m) => strip(m[1]).trim());
|
|
36
|
+
return cells.length > 0 ? `| ${cells.join(' | ')} |\n` : '';
|
|
37
|
+
});
|
|
38
|
+
// Paragraphs and divs → newlines
|
|
39
|
+
md = md.replace(/<\/p>/gi, '\n\n');
|
|
40
|
+
md = md.replace(/<br\s*\/?>/gi, '\n');
|
|
41
|
+
md = md.replace(/<\/div>/gi, '\n');
|
|
42
|
+
// Images
|
|
43
|
+
md = md.replace(/<img[^>]*alt="([^"]*)"[^>]*>/gi, (_, alt) => alt ? `[${alt}]` : '');
|
|
44
|
+
md = md.replace(/<img[^>]*>/gi, '');
|
|
45
|
+
// Strip all remaining HTML tags
|
|
46
|
+
md = md.replace(/<[^>]+>/g, '');
|
|
47
|
+
// Decode HTML entities
|
|
48
|
+
md = decodeEntities(md);
|
|
49
|
+
// Clean up whitespace
|
|
50
|
+
md = md.replace(/\n{3,}/g, '\n\n');
|
|
51
|
+
md = md.trim();
|
|
52
|
+
return md + '\n';
|
|
53
|
+
}
|
|
54
|
+
/** Strip HTML tags from a string. */
|
|
55
|
+
function strip(html) {
|
|
56
|
+
return html.replace(/<[^>]+>/g, '').trim();
|
|
57
|
+
}
|
|
58
|
+
/** Decode common HTML entities. */
|
|
59
|
+
function decodeEntities(text) {
|
|
60
|
+
return text
|
|
61
|
+
.replace(/&/g, '&')
|
|
62
|
+
.replace(/</g, '<')
|
|
63
|
+
.replace(/>/g, '>')
|
|
64
|
+
.replace(/"/g, '"')
|
|
65
|
+
.replace(/'/g, "'")
|
|
66
|
+
.replace(/→/g, '→')
|
|
67
|
+
.replace(/←/g, '←')
|
|
68
|
+
.replace(/·/g, '·')
|
|
69
|
+
.replace(/©/g, '©')
|
|
70
|
+
.replace(/\\u003c/g, '<')
|
|
71
|
+
.replace(/'/g, "'")
|
|
72
|
+
.replace(/ /g, ' ');
|
|
73
|
+
}
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuraly/lumenjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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
|
},
|
|
@@ -33,8 +33,8 @@ export async function loader() {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export class PageIndex extends LitElement {
|
|
36
|
-
static properties = {
|
|
37
|
-
|
|
36
|
+
static properties = { posts: { type: Array } };
|
|
37
|
+
posts: any[] = [];
|
|
38
38
|
|
|
39
39
|
static styles = css`
|
|
40
40
|
:host { display: block; }
|
|
@@ -49,7 +49,7 @@ export class PageIndex extends LitElement {
|
|
|
49
49
|
`;
|
|
50
50
|
|
|
51
51
|
render() {
|
|
52
|
-
const posts = this.
|
|
52
|
+
const posts = this.posts || [];
|
|
53
53
|
return html`
|
|
54
54
|
<h1>Blog</h1>
|
|
55
55
|
<p class="subtitle">Thoughts and tutorials</p>
|
|
@@ -29,8 +29,19 @@ export async function loader({ params }: { params: { slug: string } }) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export class PagePost extends LitElement {
|
|
32
|
-
static properties = {
|
|
33
|
-
|
|
32
|
+
static properties = {
|
|
33
|
+
title: { type: String },
|
|
34
|
+
date: { type: String },
|
|
35
|
+
content: { type: String },
|
|
36
|
+
readingTime: { type: Number },
|
|
37
|
+
notFound: { type: Boolean },
|
|
38
|
+
slug: { type: String },
|
|
39
|
+
};
|
|
40
|
+
title = '';
|
|
41
|
+
date = '';
|
|
42
|
+
content = '';
|
|
43
|
+
readingTime = 0;
|
|
44
|
+
notFound = false;
|
|
34
45
|
slug = '';
|
|
35
46
|
|
|
36
47
|
static styles = css`
|
|
@@ -44,7 +55,7 @@ export class PagePost extends LitElement {
|
|
|
44
55
|
`;
|
|
45
56
|
|
|
46
57
|
render() {
|
|
47
|
-
if (this.
|
|
58
|
+
if (this.notFound) {
|
|
48
59
|
return html`
|
|
49
60
|
<a class="back" href="/posts">← Back to posts</a>
|
|
50
61
|
<p class="not-found">Post not found.</p>
|
|
@@ -52,9 +63,9 @@ export class PagePost extends LitElement {
|
|
|
52
63
|
}
|
|
53
64
|
return html`
|
|
54
65
|
<a class="back" href="/">← Back to posts</a>
|
|
55
|
-
<h1>${this.
|
|
56
|
-
<div class="date">${this.
|
|
57
|
-
<p class="content">${this.
|
|
66
|
+
<h1>${this.title}</h1>
|
|
67
|
+
<div class="date">${this.date} · ${this.readingTime} min read</div>
|
|
68
|
+
<p class="content">${this.content}</p>
|
|
58
69
|
`;
|
|
59
70
|
}
|
|
60
71
|
}
|
|
@@ -11,8 +11,9 @@ export async function loader({ params }: { params: { tag: string } }) {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export class PageTag extends LitElement {
|
|
14
|
-
static properties = {
|
|
15
|
-
|
|
14
|
+
static properties = { tag: { type: String }, posts: { type: Array } };
|
|
15
|
+
tag = '';
|
|
16
|
+
posts: any[] = [];
|
|
16
17
|
|
|
17
18
|
static styles = css`
|
|
18
19
|
:host { display: block; }
|
|
@@ -28,12 +29,11 @@ export class PageTag extends LitElement {
|
|
|
28
29
|
`;
|
|
29
30
|
|
|
30
31
|
render() {
|
|
31
|
-
const { tag, posts } = this.loaderData;
|
|
32
32
|
return html`
|
|
33
33
|
<a class="back" href="/">← All posts</a>
|
|
34
|
-
<h1>Tagged: ${tag}</h1>
|
|
35
|
-
<p class="subtitle">${posts?.length || 0} post${posts?.length !== 1 ? 's' : ''}</p>
|
|
36
|
-
${(posts || []).map((p: any) => html`
|
|
34
|
+
<h1>Tagged: ${this.tag}</h1>
|
|
35
|
+
<p class="subtitle">${this.posts?.length || 0} post${this.posts?.length !== 1 ? 's' : ''}</p>
|
|
36
|
+
${(this.posts || []).map((p: any) => html`
|
|
37
37
|
<div class="post">
|
|
38
38
|
<a href="/posts/${p.slug}">${p.title}</a>
|
|
39
39
|
<div class="meta">${p.date}</div>
|
|
@@ -31,11 +31,11 @@ export function subscribe({ push }: { push: (data: any) => void }) {
|
|
|
31
31
|
|
|
32
32
|
export class PageIndex extends LitElement {
|
|
33
33
|
static properties = {
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
stats: { type: Array },
|
|
35
|
+
updatedAt: { type: String },
|
|
36
36
|
};
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
stats: any[] = [];
|
|
38
|
+
updatedAt = '';
|
|
39
39
|
|
|
40
40
|
static styles = css`
|
|
41
41
|
:host { display: block; }
|
|
@@ -50,8 +50,8 @@ export class PageIndex extends LitElement {
|
|
|
50
50
|
`;
|
|
51
51
|
|
|
52
52
|
render() {
|
|
53
|
-
const stats = this.
|
|
54
|
-
const isLive = !!this.
|
|
53
|
+
const stats = this.stats || [];
|
|
54
|
+
const isLive = !!this.updatedAt;
|
|
55
55
|
return html`
|
|
56
56
|
<h1>Overview</h1>
|
|
57
57
|
<div class="grid">
|
|
@@ -64,7 +64,7 @@ export class PageIndex extends LitElement {
|
|
|
64
64
|
</div>
|
|
65
65
|
${isLive ? html`
|
|
66
66
|
<div class="status">
|
|
67
|
-
<span class="dot"></span>Live — updated ${this.
|
|
67
|
+
<span class="dot"></span>Live — updated ${this.updatedAt ? new Date(this.updatedAt).toLocaleTimeString() : ''}
|
|
68
68
|
</div>
|
|
69
69
|
` : ''}
|
|
70
70
|
`;
|
|
@@ -5,8 +5,8 @@ export async function loader() {
|
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export class PageIndex extends LitElement {
|
|
8
|
-
static properties = {
|
|
9
|
-
|
|
8
|
+
static properties = { title: { type: String } };
|
|
9
|
+
title = '';
|
|
10
10
|
|
|
11
11
|
static styles = css`
|
|
12
12
|
:host { display: block; max-width: 640px; margin: 0 auto; padding: 2rem; font-family: system-ui; }
|
|
@@ -17,7 +17,7 @@ export class PageIndex extends LitElement {
|
|
|
17
17
|
|
|
18
18
|
render() {
|
|
19
19
|
return html`
|
|
20
|
-
<h1>${this.
|
|
20
|
+
<h1>${this.title}</h1>
|
|
21
21
|
<p>Edit <code>pages/index.ts</code> to get started.</p>
|
|
22
22
|
`;
|
|
23
23
|
}
|