@nuraly/lumenjs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +297 -0
  2. package/dist/build/build.d.ts +5 -0
  3. package/dist/build/build.js +172 -0
  4. package/dist/build/error-page.d.ts +1 -0
  5. package/dist/build/error-page.js +74 -0
  6. package/dist/build/scan.d.ts +21 -0
  7. package/dist/build/scan.js +93 -0
  8. package/dist/build/serve-api.d.ts +3 -0
  9. package/dist/build/serve-api.js +56 -0
  10. package/dist/build/serve-loaders.d.ts +4 -0
  11. package/dist/build/serve-loaders.js +115 -0
  12. package/dist/build/serve-ssr.d.ts +7 -0
  13. package/dist/build/serve-ssr.js +121 -0
  14. package/dist/build/serve-static.d.ts +6 -0
  15. package/dist/build/serve-static.js +80 -0
  16. package/dist/build/serve.d.ts +5 -0
  17. package/dist/build/serve.js +79 -0
  18. package/dist/cli.d.ts +2 -0
  19. package/dist/cli.js +65 -0
  20. package/dist/dev-server/config.d.ts +25 -0
  21. package/dist/dev-server/config.js +55 -0
  22. package/dist/dev-server/index-html.d.ts +16 -0
  23. package/dist/dev-server/index-html.js +46 -0
  24. package/dist/dev-server/nuralyui-aliases.d.ts +16 -0
  25. package/dist/dev-server/nuralyui-aliases.js +164 -0
  26. package/dist/dev-server/plugins/vite-plugin-api-routes.d.ts +23 -0
  27. package/dist/dev-server/plugins/vite-plugin-api-routes.js +250 -0
  28. package/dist/dev-server/plugins/vite-plugin-auto-import.d.ts +5 -0
  29. package/dist/dev-server/plugins/vite-plugin-auto-import.js +47 -0
  30. package/dist/dev-server/plugins/vite-plugin-lit-dedup.d.ts +5 -0
  31. package/dist/dev-server/plugins/vite-plugin-lit-dedup.js +62 -0
  32. package/dist/dev-server/plugins/vite-plugin-lit-hmr.d.ts +5 -0
  33. package/dist/dev-server/plugins/vite-plugin-lit-hmr.js +46 -0
  34. package/dist/dev-server/plugins/vite-plugin-loaders.d.ts +38 -0
  35. package/dist/dev-server/plugins/vite-plugin-loaders.js +320 -0
  36. package/dist/dev-server/plugins/vite-plugin-routes.d.ts +21 -0
  37. package/dist/dev-server/plugins/vite-plugin-routes.js +157 -0
  38. package/dist/dev-server/plugins/vite-plugin-source-annotator.d.ts +5 -0
  39. package/dist/dev-server/plugins/vite-plugin-source-annotator.js +39 -0
  40. package/dist/dev-server/plugins/vite-plugin-virtual-modules.d.ts +5 -0
  41. package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +38 -0
  42. package/dist/dev-server/server.d.ts +23 -0
  43. package/dist/dev-server/server.js +155 -0
  44. package/dist/dev-server/ssr-render.d.ts +20 -0
  45. package/dist/dev-server/ssr-render.js +170 -0
  46. package/dist/editor/click-select.d.ts +1 -0
  47. package/dist/editor/click-select.js +46 -0
  48. package/dist/editor/editor-bridge.d.ts +17 -0
  49. package/dist/editor/editor-bridge.js +101 -0
  50. package/dist/editor/element-annotator.d.ts +33 -0
  51. package/dist/editor/element-annotator.js +83 -0
  52. package/dist/editor/hover-detect.d.ts +1 -0
  53. package/dist/editor/hover-detect.js +36 -0
  54. package/dist/editor/inline-text-edit.d.ts +1 -0
  55. package/dist/editor/inline-text-edit.js +114 -0
  56. package/dist/integrations/add.d.ts +1 -0
  57. package/dist/integrations/add.js +89 -0
  58. package/dist/runtime/app-shell.d.ts +1 -0
  59. package/dist/runtime/app-shell.js +22 -0
  60. package/dist/runtime/response.d.ts +15 -0
  61. package/dist/runtime/response.js +13 -0
  62. package/dist/runtime/router-data.d.ts +3 -0
  63. package/dist/runtime/router-data.js +40 -0
  64. package/dist/runtime/router-hydration.d.ts +10 -0
  65. package/dist/runtime/router-hydration.js +68 -0
  66. package/dist/runtime/router.d.ts +35 -0
  67. package/dist/runtime/router.js +202 -0
  68. package/dist/shared/dom-shims.d.ts +5 -0
  69. package/dist/shared/dom-shims.js +63 -0
  70. package/dist/shared/route-matching.d.ts +6 -0
  71. package/dist/shared/route-matching.js +44 -0
  72. package/dist/shared/types.d.ts +16 -0
  73. package/dist/shared/types.js +1 -0
  74. package/dist/shared/utils.d.ts +42 -0
  75. package/dist/shared/utils.js +109 -0
  76. package/package.json +53 -0
@@ -0,0 +1,46 @@
1
+ import { escapeHtml } from '../shared/utils.js';
2
+ /**
3
+ * Generates the index.html shell that loads the LumenJS app.
4
+ * Includes the router, app shell, and optionally the editor bridge.
5
+ */
6
+ export function generateIndexHtml(options) {
7
+ const editorScript = options.editorMode
8
+ ? `<script type="module" src="/@lumenjs/editor-bridge"></script>`
9
+ : '';
10
+ const isSSR = !!options.ssrContent;
11
+ const appTag = isSSR
12
+ ? `<nk-app data-nk-ssr><div id="nk-router-outlet">${options.ssrContent}</div></nk-app>`
13
+ : '<nk-app></nk-app>';
14
+ // Build SSR data: if layouts are present, use structured format { page, layouts }
15
+ let loaderDataScript = '';
16
+ if (isSSR && (options.loaderData !== undefined || options.layoutsData)) {
17
+ const ssrData = options.layoutsData
18
+ ? { page: options.loaderData, layouts: options.layoutsData }
19
+ : options.loaderData;
20
+ loaderDataScript = `<script type="application/json" id="__nk_ssr_data__">${JSON.stringify(ssrData).replace(/</g, '\\u003c')}</script>`;
21
+ }
22
+ const hydrateScript = isSSR
23
+ ? `<script type="module">import '@lit-labs/ssr-client/lit-element-hydrate-support.js';</script>`
24
+ : '';
25
+ return `<!DOCTYPE html>
26
+ <html lang="en">
27
+ <head>
28
+ <meta charset="UTF-8" />
29
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
30
+ <title>${escapeHtml(options.title)}</title>
31
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@nuralyui/themes@latest/dist/default.css">${options.integrations?.includes('tailwind') ? '\n <link rel="stylesheet" href="/styles/tailwind.css">' : ''}
32
+ <style>
33
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
34
+ body { font-family: system-ui, -apple-system, sans-serif; min-height: 100vh; }
35
+ nk-app { display: block; min-height: 100vh; }
36
+ </style>
37
+ </head>
38
+ <body>
39
+ ${loaderDataScript}
40
+ ${appTag}
41
+ ${hydrateScript}
42
+ <script type="module" src="/@lumenjs/app-shell"></script>
43
+ ${editorScript}
44
+ </body>
45
+ </html>`;
46
+ }
@@ -0,0 +1,16 @@
1
+ export declare const tagToPackage: Record<string, string>;
2
+ export declare const implicitDeps: Record<string, string[]>;
3
+ /**
4
+ * NuralyUI component alias map — mirrors the studio astro.config.mjs aliases.
5
+ * Points to the component source directories within the studio service.
6
+ */
7
+ export declare function getNuralyUIAliases(nuralyUIPath: string, nuralyCommonPath: string): Record<string, string>;
8
+ /**
9
+ * Resolves the NuralyUI source path dynamically.
10
+ * Walks up from the project directory looking for the nuraly-ui package,
11
+ * then falls back to well-known paths and environment variable.
12
+ */
13
+ export declare function resolveNuralyUIPaths(projectDir: string): {
14
+ componentsPath: string;
15
+ commonPath: string;
16
+ } | null;
@@ -0,0 +1,164 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import { fileURLToPath } from 'url';
4
+ const __filename = fileURLToPath(import.meta.url);
5
+ const __dirname = path.dirname(__filename);
6
+ export const tagToPackage = {
7
+ 'nr-alert': '@nuralyui/alert',
8
+ 'nr-badge': '@nuralyui/badge',
9
+ 'nr-breadcrumb': '@nuralyui/breadcrumb',
10
+ 'nr-button': '@nuralyui/button',
11
+ 'nr-canvas': '@nuralyui/canvas',
12
+ 'nr-card': '@nuralyui/card',
13
+ 'nr-chatbot': '@nuralyui/chatbot',
14
+ 'nr-checkbox': '@nuralyui/checkbox',
15
+ 'nr-collapse': '@nuralyui/collapse',
16
+ 'nr-color-picker': '@nuralyui/color-picker',
17
+ 'nr-datepicker': '@nuralyui/datepicker',
18
+ 'nr-divider': '@nuralyui/divider',
19
+ 'nr-document': '@nuralyui/document',
20
+ 'nr-dropdown': '@nuralyui/dropdown',
21
+ 'nr-file-upload': '@nuralyui/file-upload',
22
+ 'nr-flex': '@nuralyui/flex',
23
+ 'nr-form': '@nuralyui/forms',
24
+ 'nr-grid': '@nuralyui/grid',
25
+ 'nr-icon': '@nuralyui/icon',
26
+ 'nr-image': '@nuralyui/image',
27
+ 'nr-input': '@nuralyui/input',
28
+ 'nr-label': '@nuralyui/label',
29
+ 'nr-layout': '@nuralyui/layout',
30
+ 'nr-menu': '@nuralyui/menu',
31
+ 'nr-modal': '@nuralyui/modal',
32
+ 'nr-panel': '@nuralyui/panel',
33
+ 'nr-popconfirm': '@nuralyui/popconfirm',
34
+ 'nr-radio': '@nuralyui/radio',
35
+ 'nr-select': '@nuralyui/select',
36
+ 'nr-skeleton': '@nuralyui/skeleton',
37
+ 'nr-slider-input': '@nuralyui/slider-input',
38
+ 'nr-table': '@nuralyui/table',
39
+ 'nr-tabs': '@nuralyui/tabs',
40
+ 'nr-tag': '@nuralyui/tag',
41
+ 'nr-textarea': '@nuralyui/textarea',
42
+ 'nr-timeline': '@nuralyui/timeline',
43
+ 'nr-toast': '@nuralyui/toast',
44
+ 'nr-video': '@nuralyui/video',
45
+ 'nr-radio-group': '@nuralyui/radio-group',
46
+ 'nr-iconpicker': '@nuralyui/iconpicker',
47
+ 'nr-container': '@nuralyui/container',
48
+ 'nr-code-editor': '@nuralyui/code-editor',
49
+ };
50
+ export const implicitDeps = {
51
+ 'nr-button': ['nr-icon'],
52
+ 'nr-alert': ['nr-icon'],
53
+ 'nr-breadcrumb': ['nr-icon'],
54
+ 'nr-dropdown': ['nr-icon'],
55
+ 'nr-modal': ['nr-icon'],
56
+ 'nr-popconfirm': ['nr-icon', 'nr-button'],
57
+ 'nr-select': ['nr-icon'],
58
+ 'nr-datepicker': ['nr-icon'],
59
+ 'nr-file-upload': ['nr-icon', 'nr-button'],
60
+ 'nr-collapse': ['nr-icon'],
61
+ 'nr-menu': ['nr-icon'],
62
+ 'nr-tabs': ['nr-icon'],
63
+ 'nr-toast': ['nr-icon'],
64
+ 'nr-input': ['nr-icon'],
65
+ 'nr-textarea': ['nr-icon'],
66
+ 'nr-table': ['nr-icon', 'nr-checkbox'],
67
+ 'nr-iconpicker': ['nr-icon'],
68
+ };
69
+ /**
70
+ * NuralyUI component alias map — mirrors the studio astro.config.mjs aliases.
71
+ * Points to the component source directories within the studio service.
72
+ */
73
+ export function getNuralyUIAliases(nuralyUIPath, nuralyCommonPath) {
74
+ return {
75
+ '@nuralyui/alert': path.join(nuralyUIPath, 'alert'),
76
+ '@nuralyui/badge': path.join(nuralyUIPath, 'badge'),
77
+ '@nuralyui/breadcrumb': path.join(nuralyUIPath, 'breadcrumb'),
78
+ '@nuralyui/button': path.join(nuralyUIPath, 'button'),
79
+ '@nuralyui/canvas': path.join(nuralyUIPath, 'canvas'),
80
+ '@nuralyui/card': path.join(nuralyUIPath, 'card'),
81
+ '@nuralyui/chatbot': path.join(nuralyUIPath, 'chatbot'),
82
+ '@nuralyui/checkbox': path.join(nuralyUIPath, 'checkbox'),
83
+ '@nuralyui/collapse': path.join(nuralyUIPath, 'collapse'),
84
+ '@nuralyui/color-picker': path.join(nuralyUIPath, 'colorpicker'),
85
+ '@nuralyui/datepicker': path.join(nuralyUIPath, 'datepicker'),
86
+ '@nuralyui/divider': path.join(nuralyUIPath, 'divider'),
87
+ '@nuralyui/document': path.join(nuralyUIPath, 'document'),
88
+ '@nuralyui/dropdown': path.join(nuralyUIPath, 'dropdown'),
89
+ '@nuralyui/file-upload': path.join(nuralyUIPath, 'file-upload'),
90
+ '@nuralyui/flex': path.join(nuralyUIPath, 'flex'),
91
+ '@nuralyui/forms': path.join(nuralyUIPath, 'form'),
92
+ '@nuralyui/grid': path.join(nuralyUIPath, 'grid'),
93
+ '@nuralyui/icon': path.join(nuralyUIPath, 'icon'),
94
+ '@nuralyui/image': path.join(nuralyUIPath, 'image'),
95
+ '@nuralyui/input': path.join(nuralyUIPath, 'input'),
96
+ '@nuralyui/label': path.join(nuralyUIPath, 'label'),
97
+ '@nuralyui/layout': path.join(nuralyUIPath, 'layout'),
98
+ '@nuralyui/menu': path.join(nuralyUIPath, 'menu'),
99
+ '@nuralyui/modal': path.join(nuralyUIPath, 'modal'),
100
+ '@nuralyui/panel': path.join(nuralyUIPath, 'panel'),
101
+ '@nuralyui/popconfirm': path.join(nuralyUIPath, 'popconfirm'),
102
+ '@nuralyui/radio': path.join(nuralyUIPath, 'radio'),
103
+ '@nuralyui/select': path.join(nuralyUIPath, 'select'),
104
+ '@nuralyui/skeleton': path.join(nuralyUIPath, 'skeleton'),
105
+ '@nuralyui/slider-input': path.join(nuralyUIPath, 'slider-input'),
106
+ '@nuralyui/table': path.join(nuralyUIPath, 'table'),
107
+ '@nuralyui/tabs': path.join(nuralyUIPath, 'tabs'),
108
+ '@nuralyui/tag': path.join(nuralyUIPath, 'tag'),
109
+ '@nuralyui/textarea': path.join(nuralyUIPath, 'textarea'),
110
+ '@nuralyui/timeline': path.join(nuralyUIPath, 'timeline'),
111
+ '@nuralyui/toast': path.join(nuralyUIPath, 'toast'),
112
+ '@nuralyui/video': path.join(nuralyUIPath, 'video'),
113
+ '@nuralyui/radio-group': path.join(nuralyUIPath, 'radio-group'),
114
+ '@nuralyui/iconpicker': path.join(nuralyUIPath, 'iconpicker'),
115
+ '@nuralyui/container': path.join(nuralyUIPath, 'container'),
116
+ '@nuralyui/code-editor': path.join(nuralyUIPath, 'code-editor'),
117
+ '@nuralyui/common/controllers': path.join(nuralyCommonPath, 'controllers.ts'),
118
+ '@nuralyui/common/mixins': path.join(nuralyCommonPath, 'mixins.ts'),
119
+ '@nuralyui/common/utils': path.join(nuralyCommonPath, 'utils.ts'),
120
+ '@nuralyui/common/themes': path.join(nuralyCommonPath, 'themes.ts'),
121
+ '@nuralyui/common': path.join(nuralyCommonPath, 'index.ts'),
122
+ };
123
+ }
124
+ /**
125
+ * Resolves the NuralyUI source path dynamically.
126
+ * Walks up from the project directory looking for the nuraly-ui package,
127
+ * then falls back to well-known paths and environment variable.
128
+ */
129
+ export function resolveNuralyUIPaths(projectDir) {
130
+ const nuralyUIRelPath = 'services/studio/src/features/runtime/components/ui/nuraly-ui';
131
+ const candidates = [
132
+ // Walk up from project dir to find the monorepo root containing services/
133
+ findMonorepoRoot(projectDir, nuralyUIRelPath),
134
+ // Relative to lumenjs lib (in-repo: libs/lumenjs → repo root)
135
+ path.resolve(__dirname, '../../..', nuralyUIRelPath),
136
+ // NURALYUI_PATH env override
137
+ process.env.NURALYUI_PATH || '',
138
+ ];
139
+ for (const base of candidates) {
140
+ if (!base)
141
+ continue;
142
+ const componentsPath = path.join(base, 'src/components');
143
+ const commonPath = path.join(base, 'packages/common/src');
144
+ if (fs.existsSync(componentsPath)) {
145
+ return { componentsPath, commonPath };
146
+ }
147
+ }
148
+ return null;
149
+ }
150
+ /**
151
+ * Walk up from a directory looking for a monorepo root that contains the given relative path.
152
+ */
153
+ function findMonorepoRoot(startDir, relPath) {
154
+ let dir = path.resolve(startDir);
155
+ const root = path.parse(dir).root;
156
+ while (dir !== root) {
157
+ const candidate = path.join(dir, relPath);
158
+ if (fs.existsSync(candidate)) {
159
+ return candidate;
160
+ }
161
+ dir = path.dirname(dir);
162
+ }
163
+ return '';
164
+ }
@@ -0,0 +1,23 @@
1
+ import { Plugin } from 'vite';
2
+ /**
3
+ * LumenJS API Routes plugin.
4
+ *
5
+ * Convention:
6
+ * api/users.ts → GET/POST/PUT/DELETE /api/users
7
+ * api/users/[id].ts → GET/POST/PUT/DELETE /api/users/:id
8
+ *
9
+ * Handler file exports named functions for each HTTP method:
10
+ *
11
+ * export async function GET(req: NkRequest) {
12
+ * return { users: ['Alice', 'Bob'] };
13
+ * }
14
+ *
15
+ * export async function POST(req: NkRequest) {
16
+ * const body = req.body;
17
+ * return { created: true };
18
+ * }
19
+ *
20
+ * Return value is JSON-serialized automatically.
21
+ * Throw to return an error: throw { status: 404, message: 'Not found' }
22
+ */
23
+ export declare function lumenApiRoutesPlugin(apiDir: string, projectDir?: string): Plugin;
@@ -0,0 +1,250 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import { readBody } from '../../shared/utils.js';
4
+ /**
5
+ * LumenJS API Routes plugin.
6
+ *
7
+ * Convention:
8
+ * api/users.ts → GET/POST/PUT/DELETE /api/users
9
+ * api/users/[id].ts → GET/POST/PUT/DELETE /api/users/:id
10
+ *
11
+ * Handler file exports named functions for each HTTP method:
12
+ *
13
+ * export async function GET(req: NkRequest) {
14
+ * return { users: ['Alice', 'Bob'] };
15
+ * }
16
+ *
17
+ * export async function POST(req: NkRequest) {
18
+ * const body = req.body;
19
+ * return { created: true };
20
+ * }
21
+ *
22
+ * Return value is JSON-serialized automatically.
23
+ * Throw to return an error: throw { status: 404, message: 'Not found' }
24
+ */
25
+ export function lumenApiRoutesPlugin(apiDir, projectDir) {
26
+ return {
27
+ name: 'lumenjs-api-routes',
28
+ configureServer(server) {
29
+ server.middlewares.use(async (req, res, next) => {
30
+ if (!req.url?.startsWith('/api/') || !req.method) {
31
+ return next();
32
+ }
33
+ // Parse URL and query string
34
+ const [pathname, queryString] = req.url.split('?');
35
+ const query = {};
36
+ if (queryString) {
37
+ for (const pair of queryString.split('&')) {
38
+ const [key, val] = pair.split('=');
39
+ query[decodeURIComponent(key)] = decodeURIComponent(val || '');
40
+ }
41
+ }
42
+ // Map /api/foo/bar → api/foo/bar.ts
43
+ const routePath = pathname.replace(/^\//, '');
44
+ const filePath = findApiFile(apiDir, routePath);
45
+ if (!filePath) {
46
+ return next();
47
+ }
48
+ try {
49
+ // Use Vite's ssrLoadModule for HMR support
50
+ const mod = await server.ssrLoadModule(filePath);
51
+ const handler = mod[req.method];
52
+ if (!handler || typeof handler !== 'function') {
53
+ res.statusCode = 405;
54
+ res.setHeader('Content-Type', 'application/json');
55
+ res.end(JSON.stringify({ error: `Method ${req.method} not allowed` }));
56
+ return;
57
+ }
58
+ // Parse request body for non-GET methods
59
+ let body = undefined;
60
+ let files = [];
61
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
62
+ const contentType = req.headers['content-type'] || '';
63
+ if (contentType.includes('multipart/form-data')) {
64
+ const parsed = await parseMultipart(req, contentType);
65
+ body = parsed.fields;
66
+ files = parsed.files;
67
+ }
68
+ else {
69
+ body = await readBody(req);
70
+ }
71
+ }
72
+ const nkRequest = {
73
+ method: req.method,
74
+ url: pathname,
75
+ query,
76
+ params: extractParams(apiDir, routePath, filePath),
77
+ body,
78
+ files,
79
+ headers: req.headers,
80
+ projectDir: projectDir || path.dirname(apiDir),
81
+ };
82
+ const result = await handler(nkRequest);
83
+ res.statusCode = 200;
84
+ res.setHeader('Content-Type', 'application/json');
85
+ res.end(JSON.stringify(result));
86
+ }
87
+ catch (err) {
88
+ const status = err?.status || 500;
89
+ const message = err?.message || 'Internal server error';
90
+ res.statusCode = status;
91
+ res.setHeader('Content-Type', 'application/json');
92
+ res.end(JSON.stringify({ error: message }));
93
+ }
94
+ });
95
+ },
96
+ };
97
+ }
98
+ /**
99
+ * Find the .ts/.js file for a given API route path.
100
+ * Supports dynamic segments: api/users/[id].ts matches api/users/123
101
+ */
102
+ function findApiFile(apiDir, routePath) {
103
+ // routePath = "api/foo/bar" → look for apiDir/foo/bar.ts
104
+ const relative = routePath.replace(/^api\/?/, '');
105
+ const segments = relative ? relative.split('/') : ['index'];
106
+ // Try exact match first
107
+ const exactPath = path.join(apiDir, ...segments);
108
+ for (const ext of ['.ts', '.js']) {
109
+ if (fs.existsSync(exactPath + ext)) {
110
+ return exactPath + ext;
111
+ }
112
+ }
113
+ // Try index file in directory
114
+ const indexPath = path.join(apiDir, ...segments, 'index');
115
+ for (const ext of ['.ts', '.js']) {
116
+ if (fs.existsSync(indexPath + ext)) {
117
+ return indexPath + ext;
118
+ }
119
+ }
120
+ // Try dynamic segments: walk directories, match [param] patterns
121
+ return findDynamicFile(apiDir, segments);
122
+ }
123
+ function findDynamicFile(baseDir, segments) {
124
+ if (segments.length === 0)
125
+ return null;
126
+ if (!fs.existsSync(baseDir))
127
+ return null;
128
+ const [current, ...rest] = segments;
129
+ const entries = fs.readdirSync(baseDir, { withFileTypes: true });
130
+ if (rest.length === 0) {
131
+ // Last segment — look for matching file
132
+ for (const entry of entries) {
133
+ if (!entry.isFile())
134
+ continue;
135
+ const name = entry.name.replace(/\.(ts|js)$/, '');
136
+ if (name === current || /^\[.+\]$/.test(name)) {
137
+ return path.join(baseDir, entry.name);
138
+ }
139
+ }
140
+ return null;
141
+ }
142
+ // More segments — look for matching directory
143
+ for (const entry of entries) {
144
+ if (!entry.isDirectory())
145
+ continue;
146
+ if (entry.name === current || /^\[.+\]$/.test(entry.name)) {
147
+ const result = findDynamicFile(path.join(baseDir, entry.name), rest);
148
+ if (result)
149
+ return result;
150
+ }
151
+ }
152
+ return null;
153
+ }
154
+ /**
155
+ * Extract dynamic params by comparing the URL segments with the file path segments.
156
+ */
157
+ function extractParams(apiDir, routePath, filePath) {
158
+ const params = {};
159
+ const urlSegments = routePath.replace(/^api\/?/, '').split('/').filter(Boolean);
160
+ const fileRelative = path.relative(apiDir, filePath).replace(/\.(ts|js)$/, '');
161
+ const fileSegments = fileRelative.split(path.sep);
162
+ for (let i = 0; i < fileSegments.length && i < urlSegments.length; i++) {
163
+ const match = fileSegments[i].match(/^\[(.+)\]$/);
164
+ if (match) {
165
+ params[match[1]] = urlSegments[i];
166
+ }
167
+ }
168
+ return params;
169
+ }
170
+ function readRawBody(req) {
171
+ return new Promise((resolve, reject) => {
172
+ const chunks = [];
173
+ req.on('data', (chunk) => chunks.push(chunk));
174
+ req.on('end', () => resolve(Buffer.concat(chunks)));
175
+ req.on('error', reject);
176
+ });
177
+ }
178
+ /**
179
+ * Parse multipart/form-data without external dependencies.
180
+ * Handles both file fields and text fields.
181
+ */
182
+ async function parseMultipart(req, contentType) {
183
+ const boundaryMatch = contentType.match(/boundary=(?:"([^"]+)"|([^\s;]+))/);
184
+ if (!boundaryMatch) {
185
+ return { fields: {}, files: [] };
186
+ }
187
+ const boundary = boundaryMatch[1] || boundaryMatch[2];
188
+ const raw = await readRawBody(req);
189
+ const fields = {};
190
+ const files = [];
191
+ const delimiter = Buffer.from(`--${boundary}`);
192
+ const end = Buffer.from(`--${boundary}--`);
193
+ // Split body by boundary
194
+ let start = bufferIndexOf(raw, delimiter, 0);
195
+ if (start === -1)
196
+ return { fields, files };
197
+ start += delimiter.length + 2; // skip boundary + CRLF
198
+ while (true) {
199
+ const nextBoundary = bufferIndexOf(raw, delimiter, start);
200
+ if (nextBoundary === -1)
201
+ break;
202
+ // Part content is between start and nextBoundary - 2 (strip trailing CRLF)
203
+ const partData = raw.subarray(start, nextBoundary - 2);
204
+ // Split headers from body (double CRLF)
205
+ const headerEnd = bufferIndexOf(partData, Buffer.from('\r\n\r\n'), 0);
206
+ if (headerEnd === -1) {
207
+ start = nextBoundary + delimiter.length + 2;
208
+ continue;
209
+ }
210
+ const headerStr = partData.subarray(0, headerEnd).toString('utf-8');
211
+ const body = partData.subarray(headerEnd + 4);
212
+ // Parse Content-Disposition
213
+ const nameMatch = headerStr.match(/name="([^"]+)"/);
214
+ const fileNameMatch = headerStr.match(/filename="([^"]+)"/);
215
+ const ctMatch = headerStr.match(/Content-Type:\s*(.+)/i);
216
+ if (nameMatch) {
217
+ if (fileNameMatch) {
218
+ files.push({
219
+ fieldName: nameMatch[1],
220
+ fileName: fileNameMatch[1],
221
+ contentType: ctMatch ? ctMatch[1].trim() : 'application/octet-stream',
222
+ data: Buffer.from(body),
223
+ size: body.length,
224
+ });
225
+ }
226
+ else {
227
+ fields[nameMatch[1]] = body.toString('utf-8');
228
+ }
229
+ }
230
+ // Check if next is the end boundary
231
+ if (bufferIndexOf(raw, end, nextBoundary) === nextBoundary)
232
+ break;
233
+ start = nextBoundary + delimiter.length + 2;
234
+ }
235
+ return { fields, files };
236
+ }
237
+ function bufferIndexOf(buf, search, from) {
238
+ for (let i = from; i <= buf.length - search.length; i++) {
239
+ let found = true;
240
+ for (let j = 0; j < search.length; j++) {
241
+ if (buf[i + j] !== search[j]) {
242
+ found = false;
243
+ break;
244
+ }
245
+ }
246
+ if (found)
247
+ return i;
248
+ }
249
+ return -1;
250
+ }
@@ -0,0 +1,5 @@
1
+ import { Plugin } from 'vite';
2
+ /**
3
+ * Auto-import NuralyUI components based on nr-* tags used in html`` templates.
4
+ */
5
+ export declare function autoImportPlugin(projectDir: string): Plugin;
@@ -0,0 +1,47 @@
1
+ import { tagToPackage, implicitDeps } from '../nuralyui-aliases.js';
2
+ /**
3
+ * Auto-import NuralyUI components based on nr-* tags used in html`` templates.
4
+ */
5
+ export function autoImportPlugin(projectDir) {
6
+ return {
7
+ name: 'lumenjs-auto-import',
8
+ transform(code, id) {
9
+ if (!id.startsWith(projectDir) || !id.endsWith('.ts'))
10
+ return;
11
+ if (!code.includes('html`'))
12
+ return;
13
+ const directTags = new Set();
14
+ const depTags = new Set();
15
+ for (const [tag] of Object.entries(tagToPackage)) {
16
+ if (code.includes(`<${tag}`) && !code.includes(`'${tagToPackage[tag]}'`) && !code.includes(`"${tagToPackage[tag]}"`)) {
17
+ directTags.add(tag);
18
+ const deps = implicitDeps[tag];
19
+ if (deps) {
20
+ for (const dep of deps) {
21
+ depTags.add(dep);
22
+ }
23
+ }
24
+ }
25
+ }
26
+ // Import dependencies BEFORE the components that need them.
27
+ // ES module side effects (customElements.define) run in source order,
28
+ // so nr-icon must be registered before nr-button upgrades SSR elements.
29
+ const imports = [];
30
+ for (const tag of depTags) {
31
+ const pkg = tagToPackage[tag];
32
+ if (pkg && !directTags.has(tag) && !code.includes(`'${pkg}'`) && !code.includes(`"${pkg}"`)) {
33
+ imports.push(`import '${pkg}';`);
34
+ }
35
+ }
36
+ for (const tag of directTags) {
37
+ const pkg = tagToPackage[tag];
38
+ if (pkg && !code.includes(`'${pkg}'`) && !code.includes(`"${pkg}"`)) {
39
+ imports.push(`import '${pkg}';`);
40
+ }
41
+ }
42
+ if (imports.length === 0)
43
+ return;
44
+ return { code: imports.join('\n') + '\n' + code, map: null };
45
+ }
46
+ };
47
+ }
@@ -0,0 +1,5 @@
1
+ import { Plugin } from 'vite';
2
+ /**
3
+ * Force ALL lit imports to resolve to lumenjs's single lit copy.
4
+ */
5
+ export declare function litDedupPlugin(lumenNodeModules: string, isDev: boolean): Plugin;
@@ -0,0 +1,62 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ /**
4
+ * Force ALL lit imports to resolve to lumenjs's single lit copy.
5
+ */
6
+ export function litDedupPlugin(lumenNodeModules, isDev) {
7
+ return {
8
+ name: 'lumenjs-lit-dedup',
9
+ enforce: 'pre',
10
+ resolveId(source, importer, options) {
11
+ if (!importer)
12
+ return;
13
+ const isLitImport = source === 'lit' || source.startsWith('lit/')
14
+ || source === 'lit-html' || source.startsWith('lit-html/')
15
+ || source === 'lit-element' || source.startsWith('lit-element/')
16
+ || source === '@lit/reactive-element' || source.startsWith('@lit/reactive-element/')
17
+ || source === '@lit-labs/ssr' || source.startsWith('@lit-labs/ssr/')
18
+ || source === '@lit-labs/ssr-client' || source.startsWith('@lit-labs/ssr-client/')
19
+ || source === '@lit-labs/ssr-dom-shim' || source.startsWith('@lit-labs/ssr-dom-shim/');
20
+ if (!isLitImport)
21
+ return;
22
+ const parts = source.split('/');
23
+ const pkgName = source.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
24
+ const subpath = source.startsWith('@') ? parts.slice(2).join('/') : parts.slice(1).join('/');
25
+ const pkgDir = path.join(lumenNodeModules, pkgName);
26
+ if (!fs.existsSync(pkgDir))
27
+ return;
28
+ try {
29
+ const pkg = JSON.parse(fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf-8'));
30
+ const exports = pkg.exports;
31
+ if (exports) {
32
+ const exportKey = subpath ? './' + subpath : '.';
33
+ const entry = exports[exportKey];
34
+ if (entry) {
35
+ let resolved;
36
+ if (options?.ssr) {
37
+ resolved = isDev
38
+ ? (entry?.node?.development || entry?.node?.default || entry?.node || entry?.development || entry?.default || entry)
39
+ : (entry?.node?.default || entry?.node || entry?.default || entry);
40
+ }
41
+ else {
42
+ resolved = isDev
43
+ ? (entry?.browser?.development || entry?.development || entry?.browser?.default || entry?.default || entry)
44
+ : (entry?.browser?.default || entry?.browser || entry?.default || entry);
45
+ }
46
+ if (typeof resolved === 'string') {
47
+ return path.join(pkgDir, resolved);
48
+ }
49
+ }
50
+ }
51
+ if (subpath) {
52
+ return path.join(pkgDir, subpath);
53
+ }
54
+ const entry = pkg.module || pkg.main || 'index.js';
55
+ return path.join(pkgDir, entry);
56
+ }
57
+ catch {
58
+ return subpath ? path.join(pkgDir, subpath) : pkgDir;
59
+ }
60
+ }
61
+ };
62
+ }
@@ -0,0 +1,5 @@
1
+ import { Plugin } from 'vite';
2
+ /**
3
+ * Lit HMR plugin — patches existing custom element prototypes instead of re-registering.
4
+ */
5
+ export declare function litHmrPlugin(projectDir: string): Plugin;