@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.
- package/README.md +297 -0
- package/dist/build/build.d.ts +5 -0
- package/dist/build/build.js +172 -0
- package/dist/build/error-page.d.ts +1 -0
- package/dist/build/error-page.js +74 -0
- package/dist/build/scan.d.ts +21 -0
- package/dist/build/scan.js +93 -0
- package/dist/build/serve-api.d.ts +3 -0
- package/dist/build/serve-api.js +56 -0
- package/dist/build/serve-loaders.d.ts +4 -0
- package/dist/build/serve-loaders.js +115 -0
- package/dist/build/serve-ssr.d.ts +7 -0
- package/dist/build/serve-ssr.js +121 -0
- package/dist/build/serve-static.d.ts +6 -0
- package/dist/build/serve-static.js +80 -0
- package/dist/build/serve.d.ts +5 -0
- package/dist/build/serve.js +79 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +65 -0
- package/dist/dev-server/config.d.ts +25 -0
- package/dist/dev-server/config.js +55 -0
- package/dist/dev-server/index-html.d.ts +16 -0
- package/dist/dev-server/index-html.js +46 -0
- package/dist/dev-server/nuralyui-aliases.d.ts +16 -0
- package/dist/dev-server/nuralyui-aliases.js +164 -0
- package/dist/dev-server/plugins/vite-plugin-api-routes.d.ts +23 -0
- package/dist/dev-server/plugins/vite-plugin-api-routes.js +250 -0
- package/dist/dev-server/plugins/vite-plugin-auto-import.d.ts +5 -0
- package/dist/dev-server/plugins/vite-plugin-auto-import.js +47 -0
- package/dist/dev-server/plugins/vite-plugin-lit-dedup.d.ts +5 -0
- package/dist/dev-server/plugins/vite-plugin-lit-dedup.js +62 -0
- package/dist/dev-server/plugins/vite-plugin-lit-hmr.d.ts +5 -0
- package/dist/dev-server/plugins/vite-plugin-lit-hmr.js +46 -0
- package/dist/dev-server/plugins/vite-plugin-loaders.d.ts +38 -0
- package/dist/dev-server/plugins/vite-plugin-loaders.js +320 -0
- package/dist/dev-server/plugins/vite-plugin-routes.d.ts +21 -0
- package/dist/dev-server/plugins/vite-plugin-routes.js +157 -0
- package/dist/dev-server/plugins/vite-plugin-source-annotator.d.ts +5 -0
- package/dist/dev-server/plugins/vite-plugin-source-annotator.js +39 -0
- package/dist/dev-server/plugins/vite-plugin-virtual-modules.d.ts +5 -0
- package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +38 -0
- package/dist/dev-server/server.d.ts +23 -0
- package/dist/dev-server/server.js +155 -0
- package/dist/dev-server/ssr-render.d.ts +20 -0
- package/dist/dev-server/ssr-render.js +170 -0
- package/dist/editor/click-select.d.ts +1 -0
- package/dist/editor/click-select.js +46 -0
- package/dist/editor/editor-bridge.d.ts +17 -0
- package/dist/editor/editor-bridge.js +101 -0
- package/dist/editor/element-annotator.d.ts +33 -0
- package/dist/editor/element-annotator.js +83 -0
- package/dist/editor/hover-detect.d.ts +1 -0
- package/dist/editor/hover-detect.js +36 -0
- package/dist/editor/inline-text-edit.d.ts +1 -0
- package/dist/editor/inline-text-edit.js +114 -0
- package/dist/integrations/add.d.ts +1 -0
- package/dist/integrations/add.js +89 -0
- package/dist/runtime/app-shell.d.ts +1 -0
- package/dist/runtime/app-shell.js +22 -0
- package/dist/runtime/response.d.ts +15 -0
- package/dist/runtime/response.js +13 -0
- package/dist/runtime/router-data.d.ts +3 -0
- package/dist/runtime/router-data.js +40 -0
- package/dist/runtime/router-hydration.d.ts +10 -0
- package/dist/runtime/router-hydration.js +68 -0
- package/dist/runtime/router.d.ts +35 -0
- package/dist/runtime/router.js +202 -0
- package/dist/shared/dom-shims.d.ts +5 -0
- package/dist/shared/dom-shims.js +63 -0
- package/dist/shared/route-matching.d.ts +6 -0
- package/dist/shared/route-matching.js +44 -0
- package/dist/shared/types.d.ts +16 -0
- package/dist/shared/types.js +1 -0
- package/dist/shared/utils.d.ts +42 -0
- package/dist/shared/utils.js +109 -0
- 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,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,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
|
+
}
|