create-nativecore 0.1.0 ā 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -18
- package/bin/index.mjs +407 -489
- package/package.json +4 -3
- package/template/.env.example +28 -0
- package/template/.htmlhintrc +14 -0
- package/template/api/data/dashboard.json +11 -0
- package/template/api/data/users.json +18 -0
- package/template/api/mockApi.js +161 -0
- package/template/assets/icon.svg +13 -0
- package/template/assets/logo.svg +25 -0
- package/template/eslint.config.js +94 -0
- package/template/index.html +137 -0
- package/template/manifest.json +19 -0
- package/template/public/.well-known/security.txt +9 -0
- package/template/public/_headers +24 -0
- package/template/public/_redirects +14 -0
- package/template/public/assets/icon.svg +13 -0
- package/template/public/assets/logo.svg +25 -0
- package/template/public/manifest.json +19 -0
- package/template/public/robots.txt +13 -0
- package/template/public/sitemap.xml +27 -0
- package/template/scripts/build-for-bots.mjs +121 -0
- package/template/scripts/convert-to-ts.mjs +106 -0
- package/template/scripts/fix-encoding.mjs +38 -0
- package/template/scripts/fix-svg-paths.mjs +32 -0
- package/template/scripts/generate-cf-router.mjs +52 -0
- package/template/scripts/inject-dev-tools.mjs +41 -0
- package/template/scripts/inject-version.mjs +65 -0
- package/template/scripts/make-component.mjs +445 -0
- package/template/scripts/make-component.mjs.backup +432 -0
- package/template/scripts/make-controller.mjs +119 -0
- package/template/scripts/make-core-component.mjs +303 -0
- package/template/scripts/make-view.mjs +346 -0
- package/template/scripts/minify.mjs +71 -0
- package/template/scripts/prepare-static-assets.mjs +141 -0
- package/template/scripts/prompt-bot-build.mjs +223 -0
- package/template/scripts/remove-component.mjs +170 -0
- package/template/scripts/remove-core-component.mjs +156 -0
- package/template/scripts/remove-dev.mjs +13 -0
- package/template/scripts/remove-view.mjs +200 -0
- package/template/scripts/strip-dev-blocks.mjs +30 -0
- package/template/scripts/watch-compile.mjs +69 -0
- package/template/server.js +1066 -0
- package/template/src/app.ts +115 -0
- package/template/src/components/appRegistry.ts +8 -0
- package/template/src/components/core/app-footer.ts +27 -0
- package/template/src/components/core/app-header.ts +175 -0
- package/template/src/components/core/app-sidebar.ts +238 -0
- package/template/src/components/core/loading-spinner.ts +25 -0
- package/template/src/components/core/nc-a.ts +313 -0
- package/template/src/components/core/nc-accordion.ts +186 -0
- package/template/src/components/core/nc-alert.ts +153 -0
- package/template/src/components/core/nc-animation.ts +1150 -0
- package/template/src/components/core/nc-autocomplete.ts +271 -0
- package/template/src/components/core/nc-avatar-group.ts +113 -0
- package/template/src/components/core/nc-avatar.ts +148 -0
- package/template/src/components/core/nc-badge.ts +86 -0
- package/template/src/components/core/nc-bottom-nav.ts +214 -0
- package/template/src/components/core/nc-breadcrumb.ts +96 -0
- package/template/src/components/core/nc-button.ts +307 -0
- package/template/src/components/core/nc-card.ts +160 -0
- package/template/src/components/core/nc-checkbox.ts +282 -0
- package/template/src/components/core/nc-chip.ts +115 -0
- package/template/src/components/core/nc-code.ts +314 -0
- package/template/src/components/core/nc-collapsible.ts +154 -0
- package/template/src/components/core/nc-color-picker.ts +268 -0
- package/template/src/components/core/nc-copy-button.ts +119 -0
- package/template/src/components/core/nc-date-picker.ts +443 -0
- package/template/src/components/core/nc-div.ts +280 -0
- package/template/src/components/core/nc-divider.ts +81 -0
- package/template/src/components/core/nc-drawer.ts +230 -0
- package/template/src/components/core/nc-dropdown.ts +178 -0
- package/template/src/components/core/nc-empty-state.ts +134 -0
- package/template/src/components/core/nc-file-upload.ts +354 -0
- package/template/src/components/core/nc-form.ts +312 -0
- package/template/src/components/core/nc-image.ts +184 -0
- package/template/src/components/core/nc-input.ts +383 -0
- package/template/src/components/core/nc-kbd.ts +48 -0
- package/template/src/components/core/nc-menu-item.ts +193 -0
- package/template/src/components/core/nc-menu.ts +376 -0
- package/template/src/components/core/nc-modal.ts +238 -0
- package/template/src/components/core/nc-nav-item.ts +151 -0
- package/template/src/components/core/nc-number-input.ts +350 -0
- package/template/src/components/core/nc-otp-input.ts +235 -0
- package/template/src/components/core/nc-pagination.ts +178 -0
- package/template/src/components/core/nc-popover.ts +260 -0
- package/template/src/components/core/nc-progress-circular.ts +119 -0
- package/template/src/components/core/nc-progress.ts +134 -0
- package/template/src/components/core/nc-radio.ts +235 -0
- package/template/src/components/core/nc-rating.ts +266 -0
- package/template/src/components/core/nc-rich-text.ts +283 -0
- package/template/src/components/core/nc-scroll-top.ts +116 -0
- package/template/src/components/core/nc-select.ts +452 -0
- package/template/src/components/core/nc-skeleton.ts +107 -0
- package/template/src/components/core/nc-slider.ts +285 -0
- package/template/src/components/core/nc-snackbar.ts +230 -0
- package/template/src/components/core/nc-splash.ts +343 -0
- package/template/src/components/core/nc-stepper.ts +247 -0
- package/template/src/components/core/nc-switch.ts +281 -0
- package/template/src/components/core/nc-tab-item.ts +138 -0
- package/template/src/components/core/nc-table.ts +279 -0
- package/template/src/components/core/nc-tabs.ts +554 -0
- package/template/src/components/core/nc-tag-input.ts +279 -0
- package/template/src/components/core/nc-textarea.ts +216 -0
- package/template/src/components/core/nc-time-picker.ts +438 -0
- package/template/src/components/core/nc-timeline.ts +186 -0
- package/template/src/components/core/nc-tooltip.ts +143 -0
- package/template/src/components/frameworkRegistry.ts +68 -0
- package/template/src/components/preloadRegistry.ts +28 -0
- package/template/src/components/registry.ts +8 -0
- package/template/src/components/ui/dashboard-signal-lab.ts +284 -0
- package/template/src/constants/apiEndpoints.ts +27 -0
- package/template/src/constants/errorMessages.ts +23 -0
- package/template/src/constants/index.ts +8 -0
- package/template/src/constants/routePaths.ts +15 -0
- package/template/src/constants/storageKeys.ts +18 -0
- package/template/src/controllers/dashboard.controller.ts +200 -0
- package/template/src/controllers/home.controller.ts +21 -0
- package/template/src/controllers/index.ts +11 -0
- package/template/src/controllers/login.controller.ts +131 -0
- package/template/src/core/component.ts +354 -0
- package/template/src/core/errorHandler.ts +85 -0
- package/template/src/core/gpu-animation.ts +604 -0
- package/template/src/core/http.ts +173 -0
- package/template/src/core/lazyComponents.ts +90 -0
- package/template/src/core/router.ts +642 -0
- package/template/src/core/signals.ts +146 -0
- package/template/src/core/state.ts +248 -0
- package/template/src/dev/component-editor.ts +1363 -0
- package/template/src/dev/component-overlay.ts +278 -0
- package/template/src/dev/context-menu.ts +223 -0
- package/template/src/dev/denc-tools.ts +250 -0
- package/template/src/dev/hmr.ts +189 -0
- package/template/src/dev/nfbs.code-workspace +27 -0
- package/template/src/dev/outline-panel.ts +1247 -0
- package/template/src/middleware/auth.middleware.ts +23 -0
- package/template/src/routes/routes.ts +38 -0
- package/template/src/services/api.service.ts +394 -0
- package/template/src/services/auth.service.ts +176 -0
- package/template/src/services/index.ts +8 -0
- package/template/src/services/logger.service.ts +74 -0
- package/template/src/services/storage.service.ts +88 -0
- package/template/src/stores/appStore.ts +57 -0
- package/template/src/stores/uiStore.ts +36 -0
- package/template/src/styles/core-variables.css +219 -0
- package/template/src/styles/core.css +710 -0
- package/template/src/styles/main.css +3164 -0
- package/template/src/styles/variables.css +152 -0
- package/template/src/types/global.d.ts +47 -0
- package/template/src/utils/cacheBuster.ts +20 -0
- package/template/src/utils/dom.ts +149 -0
- package/template/src/utils/events.ts +203 -0
- package/template/src/utils/form.ts +176 -0
- package/template/src/utils/formatters.ts +169 -0
- package/template/src/utils/helpers.ts +195 -0
- package/template/src/utils/markdown.ts +307 -0
- package/template/src/utils/sidebar.ts +96 -0
- package/template/src/utils/smoothScroll.ts +85 -0
- package/template/src/utils/templates.ts +23 -0
- package/template/src/utils/validation.ts +73 -0
- package/template/src/views/protected/dashboard.html +293 -0
- package/template/src/views/public/home.html +150 -0
- package/template/src/views/public/login.html +102 -0
- package/template/tests/unit/component.test.ts +87 -0
- package/template/tests/unit/computed.test.ts +79 -0
- package/template/tests/unit/form.test.ts +68 -0
- package/template/tests/unit/formatters.test.ts +49 -0
- package/template/tests/unit/lazy-components.test.ts +59 -0
- package/template/tests/unit/markdown.test.ts +62 -0
- package/template/tests/unit/router.test.ts +112 -0
- package/template/tests/unit/signals.test.ts +54 -0
- package/template/tests/unit/validation.test.ts +50 -0
- package/template/tsconfig.build.json +21 -0
- package/template/tsconfig.json +51 -0
- package/template/vitest.config.ts +36 -0
|
@@ -0,0 +1,1066 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple SPA Development Server
|
|
3
|
+
* Serves index.html for all routes (except static assets)
|
|
4
|
+
* Includes mock API endpoints + Hot Module Replacement (HMR)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import http from 'http';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
import { WebSocketServer } from 'ws';
|
|
12
|
+
import * as mockApi from './api/mockApi.js';
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
|
|
17
|
+
const PORT = 8000;
|
|
18
|
+
const DEV_REMOTE_API_ORIGIN = process.env.DEV_REMOTE_API_ORIGIN || '';
|
|
19
|
+
const DEV_REMOTE_AUTH_LOGIN_URL = process.env.DEV_REMOTE_AUTH_LOGIN_URL || (DEV_REMOTE_API_ORIGIN ? `${DEV_REMOTE_API_ORIGIN}/auth/login` : '');
|
|
20
|
+
const HMR_PORT = 8001;
|
|
21
|
+
const ROOT_DIR = __dirname;
|
|
22
|
+
|
|
23
|
+
const MIME_TYPES = {
|
|
24
|
+
'.html': 'text/html',
|
|
25
|
+
'.css': 'text/css',
|
|
26
|
+
'.js': 'text/javascript',
|
|
27
|
+
'.ts': 'text/javascript',
|
|
28
|
+
'.md': 'text/markdown; charset=utf-8',
|
|
29
|
+
'.json': 'application/json',
|
|
30
|
+
'.png': 'image/png',
|
|
31
|
+
'.jpg': 'image/jpeg',
|
|
32
|
+
'.gif': 'image/gif',
|
|
33
|
+
'.svg': 'image/svg+xml',
|
|
34
|
+
'.ico': 'image/x-icon'
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Parse JSON body
|
|
38
|
+
function parseBody(req) {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
let body = '';
|
|
41
|
+
req.on('data', chunk => {
|
|
42
|
+
body += chunk.toString();
|
|
43
|
+
});
|
|
44
|
+
req.on('end', () => {
|
|
45
|
+
try {
|
|
46
|
+
resolve(body ? JSON.parse(body) : {});
|
|
47
|
+
} catch (e) {
|
|
48
|
+
reject(e);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ============================================
|
|
55
|
+
// DEV TOOLS: Component Metadata Parser
|
|
56
|
+
// ============================================
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get component metadata by parsing the source file
|
|
60
|
+
*/
|
|
61
|
+
async function getComponentMetadata(tagName) {
|
|
62
|
+
// Find the component file
|
|
63
|
+
const possiblePaths = [
|
|
64
|
+
path.join(ROOT_DIR, 'src/components/ui', `${tagName}.ts`),
|
|
65
|
+
path.join(ROOT_DIR, 'src/components/core', `${tagName}.ts`),
|
|
66
|
+
path.join(ROOT_DIR, 'src/components', `${tagName}.ts`)
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
let filePath = null;
|
|
70
|
+
for (const p of possiblePaths) {
|
|
71
|
+
if (fs.existsSync(p)) {
|
|
72
|
+
filePath = p;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!filePath) {
|
|
78
|
+
const componentDirs = [
|
|
79
|
+
path.join(ROOT_DIR, 'src/components/core'),
|
|
80
|
+
path.join(ROOT_DIR, 'src/components/ui'),
|
|
81
|
+
path.join(ROOT_DIR, 'src/components'),
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const defineComponentPatterns = [
|
|
85
|
+
`defineComponent('${tagName}'`,
|
|
86
|
+
`defineComponent("${tagName}"`,
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
for (const dir of componentDirs) {
|
|
90
|
+
if (!fs.existsSync(dir)) continue;
|
|
91
|
+
|
|
92
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
if (!entry.isFile() || !entry.name.endsWith('.ts')) continue;
|
|
95
|
+
|
|
96
|
+
const candidatePath = path.join(dir, entry.name);
|
|
97
|
+
const candidateSource = fs.readFileSync(candidatePath, 'utf-8');
|
|
98
|
+
|
|
99
|
+
if (defineComponentPatterns.some(pattern => candidateSource.includes(pattern))) {
|
|
100
|
+
filePath = candidatePath;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (filePath) break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!filePath) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log(`[DEBUG] Reading component file: ${filePath}`);
|
|
114
|
+
|
|
115
|
+
const sourceCode = fs.readFileSync(filePath, 'utf-8');
|
|
116
|
+
const lines = sourceCode.split('\n');
|
|
117
|
+
|
|
118
|
+
// Parse class name
|
|
119
|
+
const classMatch = sourceCode.match(/export class (\w+) extends Component/);
|
|
120
|
+
const className = classMatch ? classMatch[1] : 'Unknown';
|
|
121
|
+
|
|
122
|
+
// Parse attributes from getAttribute calls
|
|
123
|
+
const attributes = [];
|
|
124
|
+
const attrRegex = /this\.getAttribute\(['"](\w+)['"]\)/g;
|
|
125
|
+
let attrMatch;
|
|
126
|
+
while ((attrMatch = attrRegex.exec(sourceCode)) !== null) {
|
|
127
|
+
const name = attrMatch[1];
|
|
128
|
+
const lineIndex = sourceCode.substring(0, attrMatch.index).split('\n').length;
|
|
129
|
+
|
|
130
|
+
let type = 'string';
|
|
131
|
+
let variantOptions = null;
|
|
132
|
+
|
|
133
|
+
const contextLine = lines[lineIndex - 1] || '';
|
|
134
|
+
if (contextLine.includes('parseInt') || contextLine.includes('parseFloat') || contextLine.includes('Number(')) {
|
|
135
|
+
type = 'number';
|
|
136
|
+
}
|
|
137
|
+
if (sourceCode.includes(`hasAttribute('${name}')`)) {
|
|
138
|
+
type = 'boolean';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Check for dropdown options for ANY attribute (excluding known string-only)
|
|
142
|
+
if (name !== 'href' && name !== 'src' && name !== 'alt' && name !== 'title' && name !== 'class' && name !== 'id') {
|
|
143
|
+
variantOptions = extractVariantOptions(sourceCode, name);
|
|
144
|
+
if (variantOptions && variantOptions.length > 0) {
|
|
145
|
+
type = 'variant';
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!attributes.find(a => a.name === name)) {
|
|
150
|
+
attributes.push({ name, type, defaultValue: '', currentValue: '', line: lineIndex, variantOptions });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Parse CSS variables from template
|
|
155
|
+
const cssVariables = [];
|
|
156
|
+
const cssVarRegex = /--([a-zA-Z0-9-]+)\s*:\s*([^;]+);/g;
|
|
157
|
+
let cssMatch;
|
|
158
|
+
while ((cssMatch = cssVarRegex.exec(sourceCode)) !== null) {
|
|
159
|
+
const name = `--${cssMatch[1]}`;
|
|
160
|
+
const defaultValue = cssMatch[2].trim();
|
|
161
|
+
const lineIndex = sourceCode.substring(0, cssMatch.index).split('\n').length;
|
|
162
|
+
cssVariables.push({ name, defaultValue, currentValue: defaultValue, line: lineIndex });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Parse :host styles
|
|
166
|
+
const hostStyles = [];
|
|
167
|
+
const hostMatch = sourceCode.match(/:host\s*\{([^}]+)\}/);
|
|
168
|
+
if (hostMatch) {
|
|
169
|
+
const hostContent = hostMatch[1];
|
|
170
|
+
const styleRegex = /([a-z-]+)\s*:\s*([^;]+);/gi;
|
|
171
|
+
let styleMatch;
|
|
172
|
+
while ((styleMatch = styleRegex.exec(hostContent)) !== null) {
|
|
173
|
+
const prop = styleMatch[1].trim();
|
|
174
|
+
const value = styleMatch[2].trim();
|
|
175
|
+
// Skip CSS variables (already captured)
|
|
176
|
+
if (!prop.startsWith('--')) {
|
|
177
|
+
hostStyles.push({ property: prop, value });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Parse computed styles from template (common patterns)
|
|
183
|
+
const inlineStyles = [];
|
|
184
|
+
const styleAttrRegex = /style\s*=\s*["']([^"']+)["']/g;
|
|
185
|
+
let inlineMatch;
|
|
186
|
+
while ((inlineMatch = styleAttrRegex.exec(sourceCode)) !== null) {
|
|
187
|
+
const styleContent = inlineMatch[1];
|
|
188
|
+
const props = styleContent.split(';').filter(s => s.trim());
|
|
189
|
+
props.forEach(p => {
|
|
190
|
+
const [prop, value] = p.split(':').map(s => s.trim());
|
|
191
|
+
if (prop && value && !inlineStyles.find(s => s.property === prop)) {
|
|
192
|
+
inlineStyles.push({ property: prop, value });
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Detect if component uses Shadow DOM
|
|
198
|
+
const usesShadowDOM = sourceCode.includes('static useShadowDOM = true') ||
|
|
199
|
+
sourceCode.includes('this.attachShadow');
|
|
200
|
+
|
|
201
|
+
// Detect observed attributes
|
|
202
|
+
const observedAttrsMatch = sourceCode.match(/static get observedAttributes\(\)\s*\{\s*return\s*\[([^\]]+)\]/);
|
|
203
|
+
console.log(`[DEBUG] observedAttributes match for ${tagName}:`, observedAttrsMatch ? observedAttrsMatch[1] : 'NOT FOUND');
|
|
204
|
+
if (observedAttrsMatch) {
|
|
205
|
+
const attrNames = observedAttrsMatch[1].match(/['"]([^'"]+)['"]/g);
|
|
206
|
+
if (attrNames) {
|
|
207
|
+
attrNames.forEach(name => {
|
|
208
|
+
const cleanName = name.replace(/['"]/g, '');
|
|
209
|
+
if (!attributes.find(a => a.name === cleanName)) {
|
|
210
|
+
// Determine attribute type
|
|
211
|
+
let attrType = 'string';
|
|
212
|
+
let variantOptions = null;
|
|
213
|
+
|
|
214
|
+
// Boolean attributes
|
|
215
|
+
const booleanAttrs = ['disabled', 'readonly', 'required', 'checked', 'selected', 'hidden', 'loading'];
|
|
216
|
+
if (booleanAttrs.includes(cleanName)) {
|
|
217
|
+
attrType = 'boolean';
|
|
218
|
+
}
|
|
219
|
+
// Try to extract dropdown options for ANY attribute
|
|
220
|
+
else if (cleanName !== 'href' && cleanName !== 'src' && cleanName !== 'class' && cleanName !== 'id') {
|
|
221
|
+
console.log(`[DEBUG] Extracting options for ${cleanName}...`);
|
|
222
|
+
variantOptions = extractVariantOptions(sourceCode, cleanName);
|
|
223
|
+
console.log(`[DEBUG] Extracted options for ${cleanName}:`, variantOptions);
|
|
224
|
+
if (variantOptions && variantOptions.length > 0) {
|
|
225
|
+
attrType = 'variant';
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Number attributes
|
|
229
|
+
if (['count', 'max', 'min', 'step', 'duration', 'delay', 'index'].includes(cleanName)) {
|
|
230
|
+
attrType = 'number';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
attributes.push({
|
|
234
|
+
name: cleanName,
|
|
235
|
+
type: attrType,
|
|
236
|
+
defaultValue: '',
|
|
237
|
+
currentValue: '',
|
|
238
|
+
line: 0,
|
|
239
|
+
variantOptions
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
console.warn(`[DEBUG] Added attribute: ${cleanName}, type: ${attrType}, options:`, variantOptions);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
tagName,
|
|
250
|
+
filePath: filePath.replace(ROOT_DIR + path.sep, '').replace(/\\/g, '/'),
|
|
251
|
+
absoluteFilePath: filePath,
|
|
252
|
+
className,
|
|
253
|
+
attributes,
|
|
254
|
+
cssVariables,
|
|
255
|
+
hostStyles,
|
|
256
|
+
inlineStyles,
|
|
257
|
+
usesShadowDOM,
|
|
258
|
+
slots: [],
|
|
259
|
+
sourceCode
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Extract variant options from component source
|
|
265
|
+
* Priority: 1) static attributeOptions, 2) CSS patterns, 3) code patterns
|
|
266
|
+
*/
|
|
267
|
+
function extractVariantOptions(sourceCode, attributeName) {
|
|
268
|
+
const options = new Set();
|
|
269
|
+
|
|
270
|
+
// PRIORITY 1: Check for static attributeOptions property
|
|
271
|
+
const attributeOptionsRegex = /static\s+attributeOptions\s*=\s*\{([^}]+)\}/s;
|
|
272
|
+
const attributeOptionsMatch = sourceCode.match(attributeOptionsRegex);
|
|
273
|
+
|
|
274
|
+
if (attributeOptionsMatch) {
|
|
275
|
+
const optionsBlock = attributeOptionsMatch[1];
|
|
276
|
+
// Match the specific attribute and its array
|
|
277
|
+
const attrRegex = new RegExp(`['"]?${attributeName.replace('-', '[\\-_]?')}['"]?\\s*:\\s*\\[([^\\]]+)\\]`, 'i');
|
|
278
|
+
const attrMatch = optionsBlock.match(attrRegex);
|
|
279
|
+
|
|
280
|
+
if (attrMatch) {
|
|
281
|
+
const values = attrMatch[1].match(/['"]([^'"]+)['"]/g);
|
|
282
|
+
if (values) {
|
|
283
|
+
values.forEach(v => options.add(v.replace(/['"]/g, '')));
|
|
284
|
+
console.log(`[DEBUG] Found attributeOptions for ${attributeName}:`, Array.from(options));
|
|
285
|
+
return options.size > 0 ? Array.from(options) : null; // Don't sort - preserve order
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// PRIORITY 2 & 3: Fallback to CSS/code pattern detection
|
|
291
|
+
const sizeKeywords = ['sm', 'md', 'lg', 'xl', 'xs', 'small', 'medium', 'large', 'tiny', 'huge'];
|
|
292
|
+
const variantKeywords = ['primary', 'secondary', 'success', 'danger', 'warning', 'info',
|
|
293
|
+
'light', 'dark', 'outline', 'ghost', 'link', 'text', 'error'];
|
|
294
|
+
const positionKeywords = ['left', 'right', 'top', 'bottom', 'center', 'start', 'end'];
|
|
295
|
+
|
|
296
|
+
if (attributeName === 'size') {
|
|
297
|
+
// Match patterns like: .nc-btn-sm, .size-lg, .small, etc.
|
|
298
|
+
const sizeRegex = new RegExp(`\\.(?:[a-z]+-)?(?:btn-|size-)?(${sizeKeywords.join('|')})\\s*\\{`, 'gi');
|
|
299
|
+
let match;
|
|
300
|
+
while ((match = sizeRegex.exec(sourceCode)) !== null) {
|
|
301
|
+
const size = match[1].toLowerCase();
|
|
302
|
+
options.add(size);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Match :host([size="sm"]) or :host([size='lg'])
|
|
306
|
+
const hostSizeRegex = new RegExp(`:host\\(\\[size=["'](${sizeKeywords.join('|')})["']\\]\\)`, 'gi');
|
|
307
|
+
let hostMatch;
|
|
308
|
+
while ((hostMatch = hostSizeRegex.exec(sourceCode)) !== null) {
|
|
309
|
+
const size = hostMatch[1].toLowerCase();
|
|
310
|
+
options.add(size);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Also check for comments like /* Sizes */ or /* Size: sm, md, lg */
|
|
314
|
+
const commentRegex = /\/\*\s*Sizes?\s*:?\s*\*\/[\s\S]*?(?=\/\*|$)/gi;
|
|
315
|
+
let commentMatch;
|
|
316
|
+
while ((commentMatch = commentRegex.exec(sourceCode)) !== null) {
|
|
317
|
+
const section = commentMatch[0];
|
|
318
|
+
sizeKeywords.forEach(keyword => {
|
|
319
|
+
if (section.toLowerCase().includes(keyword)) {
|
|
320
|
+
options.add(keyword);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
console.log(`[DEBUG] Size options found for ${attributeName}:`, Array.from(options));
|
|
326
|
+
} else if (attributeName === 'variant') {
|
|
327
|
+
// Match patterns like: .nc-btn-primary, .variant-success, .btn-danger, etc.
|
|
328
|
+
const variantRegex = new RegExp(`\\.(?:[a-z]+-)?(?:btn-|variant-)?(${variantKeywords.join('|')})\\s*\\{`, 'gi');
|
|
329
|
+
let match;
|
|
330
|
+
|
|
331
|
+
while ((match = variantRegex.exec(sourceCode)) !== null) {
|
|
332
|
+
const variant = match[1].toLowerCase();
|
|
333
|
+
options.add(variant);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Match :host([variant="primary"]) or :host([variant='primary'])
|
|
337
|
+
const hostVariantRegex = new RegExp(`:host\\(\\[variant=["'](${variantKeywords.join('|')})["']\\]\\)`, 'gi');
|
|
338
|
+
let hostMatch;
|
|
339
|
+
while ((hostMatch = hostVariantRegex.exec(sourceCode)) !== null) {
|
|
340
|
+
const variant = hostMatch[1].toLowerCase();
|
|
341
|
+
options.add(variant);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Also check for comments like /* Variant: Primary */ or /* Variants */
|
|
345
|
+
const commentRegex = /\/\*\s*Variants?\s*:?\s*\*\/[\s\S]*?(?=\/\*|$)/gi;
|
|
346
|
+
let commentMatch;
|
|
347
|
+
while ((commentMatch = commentRegex.exec(sourceCode)) !== null) {
|
|
348
|
+
const section = commentMatch[0];
|
|
349
|
+
variantKeywords.forEach(keyword => {
|
|
350
|
+
if (section.toLowerCase().includes(keyword)) {
|
|
351
|
+
options.add(keyword);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
console.log(`[DEBUG] Variant options found for ${attributeName}:`, Array.from(options));
|
|
357
|
+
} else if (attributeName.includes('position')) {
|
|
358
|
+
// Match iconPosition === 'left', icon-position="right", etc.
|
|
359
|
+
const positionRegex = new RegExp(`(${positionKeywords.join('|')})`, 'gi');
|
|
360
|
+
let match;
|
|
361
|
+
while ((match = positionRegex.exec(sourceCode)) !== null) {
|
|
362
|
+
const pos = match[1].toLowerCase();
|
|
363
|
+
// Only add if it's in a relevant context (near icon-position or iconPosition)
|
|
364
|
+
const contextStart = Math.max(0, match.index - 100);
|
|
365
|
+
const contextEnd = Math.min(sourceCode.length, match.index + 100);
|
|
366
|
+
const context = sourceCode.substring(contextStart, contextEnd);
|
|
367
|
+
if (context.includes('position') || context.includes('flex-direction')) {
|
|
368
|
+
options.add(pos);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
console.log(`[DEBUG] Position options found for ${attributeName}:`, Array.from(options));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return options.size > 0 ? Array.from(options).sort() : null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Edit component file with style changes
|
|
380
|
+
*/
|
|
381
|
+
async function editComponentFile({ tagName, filePath, changes, styleChanges }) {
|
|
382
|
+
const fullPath = path.join(ROOT_DIR, filePath);
|
|
383
|
+
|
|
384
|
+
if (!fs.existsSync(fullPath)) {
|
|
385
|
+
return { success: false, message: `File not found: ${filePath}` };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
let sourceCode = fs.readFileSync(fullPath, 'utf-8');
|
|
390
|
+
|
|
391
|
+
// Handle style changes - inject into component's host styles
|
|
392
|
+
if (styleChanges && Object.keys(styleChanges).length > 0) {
|
|
393
|
+
// Convert camelCase to kebab-case for CSS
|
|
394
|
+
const cssProperties = Object.entries(styleChanges)
|
|
395
|
+
.map(([prop, value]) => {
|
|
396
|
+
const kebabProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
397
|
+
return `${kebabProp}: ${value};`;
|
|
398
|
+
})
|
|
399
|
+
.join('\n ');
|
|
400
|
+
|
|
401
|
+
// Check if :host styles already exist
|
|
402
|
+
if (sourceCode.includes(':host {')) {
|
|
403
|
+
// Update existing :host block
|
|
404
|
+
sourceCode = sourceCode.replace(
|
|
405
|
+
/(:host\s*\{[^}]*)(})/,
|
|
406
|
+
`$1\n ${cssProperties}\n $2`
|
|
407
|
+
);
|
|
408
|
+
} else if (sourceCode.includes('<style>')) {
|
|
409
|
+
// Add :host block after <style> tag
|
|
410
|
+
sourceCode = sourceCode.replace(
|
|
411
|
+
/(<style>)/,
|
|
412
|
+
`$1\n :host {\n ${cssProperties}\n }`
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
console.log(`[DevTools] Style changes for <${tagName}>:`, styleChanges);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Handle legacy changes format
|
|
420
|
+
if (changes && Array.isArray(changes)) {
|
|
421
|
+
for (const change of changes) {
|
|
422
|
+
if (change.type === 'attribute') {
|
|
423
|
+
console.log(`[DevTools] Attribute change: ${change.name} = ${change.value}`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (change.type === 'cssVariable') {
|
|
427
|
+
const varRegex = new RegExp(`(${change.name}\\s*:\\s*)([^;]+)(;)`, 'g');
|
|
428
|
+
sourceCode = sourceCode.replace(varRegex, `$1${change.value}$3`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Write the file
|
|
434
|
+
fs.writeFileSync(fullPath, sourceCode, 'utf-8');
|
|
435
|
+
|
|
436
|
+
return { success: true, message: 'Component updated successfully' };
|
|
437
|
+
|
|
438
|
+
} catch (error) {
|
|
439
|
+
return { success: false, message: error.message };
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Save changes to a specific component instance in an HTML file
|
|
445
|
+
*/
|
|
446
|
+
async function saveInstanceChanges({ tagName, viewPath, attributes, inlineStyles, elementIndex }) {
|
|
447
|
+
try {
|
|
448
|
+
console.log('[DevTools] saveInstanceChanges called with:', { tagName, viewPath, attributes, elementIndex });
|
|
449
|
+
|
|
450
|
+
// Map route paths to actual HTML file paths
|
|
451
|
+
const viewsMap = {
|
|
452
|
+
'/': 'src/views/public/home.html',
|
|
453
|
+
'/about': 'src/views/public/about.html',
|
|
454
|
+
'/login': 'src/views/public/login.html',
|
|
455
|
+
'/components': 'src/views/public/components.html',
|
|
456
|
+
'/dashboard': 'src/views/protected/dashboard.html',
|
|
457
|
+
'/under-construction': 'src/views/protected/under-construction.html',
|
|
458
|
+
'/testing': 'src/views/protected/testing.html',
|
|
459
|
+
'/user/:id': 'src/views/protected/user-detail.html'
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
// Handle dynamic routes (e.g., /user/123)
|
|
463
|
+
let htmlFilePath = viewsMap[viewPath];
|
|
464
|
+
if (!htmlFilePath) {
|
|
465
|
+
// Try to match dynamic routes
|
|
466
|
+
for (const [route, file] of Object.entries(viewsMap)) {
|
|
467
|
+
if (route.includes(':')) {
|
|
468
|
+
const routePattern = route.replace(/:[^/]+/g, '[^/]+');
|
|
469
|
+
const regex = new RegExp(`^${routePattern}$`);
|
|
470
|
+
if (regex.test(viewPath)) {
|
|
471
|
+
htmlFilePath = file;
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (!htmlFilePath) {
|
|
479
|
+
console.error('[DevTools] Unknown view path:', viewPath);
|
|
480
|
+
return { success: false, message: `Unknown view path: ${viewPath}. Add it to viewsMap in server.js` };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const fullPath = path.join(ROOT_DIR, htmlFilePath);
|
|
484
|
+
if (!fs.existsSync(fullPath)) {
|
|
485
|
+
console.error('[DevTools] View file not found:', htmlFilePath);
|
|
486
|
+
return { success: false, message: `View file not found: ${htmlFilePath}` };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
let content = fs.readFileSync(fullPath, 'utf-8');
|
|
490
|
+
|
|
491
|
+
// Find the specific component tag instance
|
|
492
|
+
const tagRegex = new RegExp(`<${tagName}([^>]*)>`, 'g');
|
|
493
|
+
let matches = [];
|
|
494
|
+
let match;
|
|
495
|
+
|
|
496
|
+
while ((match = tagRegex.exec(content)) !== null) {
|
|
497
|
+
matches.push({ index: match.index, fullMatch: match[0], attrs: match[1] });
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
console.log('[DevTools] Found', matches.length, 'instances of', tagName, 'in', htmlFilePath);
|
|
501
|
+
console.log('[DevTools] Looking for elementIndex:', elementIndex);
|
|
502
|
+
|
|
503
|
+
if (elementIndex >= matches.length) {
|
|
504
|
+
console.error('[DevTools] Component instance not found. Index:', elementIndex, 'Total:', matches.length);
|
|
505
|
+
return { success: false, message: `Component instance ${elementIndex} not found (found ${matches.length} instances)` };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const targetMatch = matches[elementIndex];
|
|
509
|
+
|
|
510
|
+
// Parse existing attributes from the tag
|
|
511
|
+
const existingAttrs = {};
|
|
512
|
+
const attrRegex = /(\w+)="([^"]*)"/g;
|
|
513
|
+
let attrMatch;
|
|
514
|
+
while ((attrMatch = attrRegex.exec(targetMatch.attrs)) !== null) {
|
|
515
|
+
existingAttrs[attrMatch[1]] = attrMatch[2];
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Merge with new attributes (new values override existing)
|
|
519
|
+
const mergedAttrs = { ...existingAttrs, ...attributes };
|
|
520
|
+
|
|
521
|
+
// Build new attributes string
|
|
522
|
+
const attrPairs = [];
|
|
523
|
+
|
|
524
|
+
// Add merged attributes
|
|
525
|
+
for (const [key, value] of Object.entries(mergedAttrs)) {
|
|
526
|
+
if (key !== 'style') { // Handle style separately
|
|
527
|
+
attrPairs.push(`${key}="${value}"`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Add inline styles as style attribute
|
|
532
|
+
if (inlineStyles && Object.keys(inlineStyles).length > 0) {
|
|
533
|
+
const styleString = Object.entries(inlineStyles)
|
|
534
|
+
.map(([prop, value]) => {
|
|
535
|
+
const kebabProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
536
|
+
return `${kebabProp}: ${value}`;
|
|
537
|
+
})
|
|
538
|
+
.join('; ');
|
|
539
|
+
attrPairs.push(`style="${styleString}"`);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const newTag = attrPairs.length > 0
|
|
543
|
+
? `<${tagName} ${attrPairs.join(' ')}>`
|
|
544
|
+
: `<${tagName}>`;
|
|
545
|
+
|
|
546
|
+
// Replace the specific instance
|
|
547
|
+
content = content.substring(0, targetMatch.index) + newTag + content.substring(targetMatch.index + targetMatch.fullMatch.length);
|
|
548
|
+
|
|
549
|
+
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
550
|
+
|
|
551
|
+
console.log(`[DevTools] Saved instance changes for <${tagName}> in ${htmlFilePath}`);
|
|
552
|
+
|
|
553
|
+
// Trigger HMR to update the page
|
|
554
|
+
notifyHMRClients(htmlFilePath);
|
|
555
|
+
|
|
556
|
+
return { success: true, message: 'Instance changes saved successfully' };
|
|
557
|
+
|
|
558
|
+
} catch (error) {
|
|
559
|
+
console.error('[DevTools] Error saving instance changes:', error);
|
|
560
|
+
return { success: false, message: error.message };
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Save changes globally to the component TypeScript file
|
|
566
|
+
*/
|
|
567
|
+
async function saveGlobalChanges({ tagName, filePath, defaultAttributes, styleChanges }) {
|
|
568
|
+
try {
|
|
569
|
+
const fullPath = path.join(ROOT_DIR, filePath);
|
|
570
|
+
|
|
571
|
+
if (!fs.existsSync(fullPath)) {
|
|
572
|
+
return { success: false, message: `Component file not found: ${filePath}` };
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
let sourceCode = fs.readFileSync(fullPath, 'utf-8');
|
|
576
|
+
|
|
577
|
+
// Update default attribute values in the template() method
|
|
578
|
+
if (defaultAttributes && Object.keys(defaultAttributes).length > 0) {
|
|
579
|
+
for (const [attrName, attrValue] of Object.entries(defaultAttributes)) {
|
|
580
|
+
// Look for this.attr() calls or getAttribute() calls in template
|
|
581
|
+
const attrPattern = new RegExp(`(this\\.attr\\(['"]${attrName}['"],\\s*['"])([^'"]+)(['"]\\))`, 'g');
|
|
582
|
+
if (sourceCode.match(attrPattern)) {
|
|
583
|
+
sourceCode = sourceCode.replace(attrPattern, `$1${attrValue}$3`);
|
|
584
|
+
console.log(`[DevTools] Updated default for ${attrName} to ${attrValue}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Handle style changes in :host block
|
|
590
|
+
if (styleChanges && Object.keys(styleChanges).length > 0) {
|
|
591
|
+
const cssProperties = Object.entries(styleChanges)
|
|
592
|
+
.map(([prop, value]) => {
|
|
593
|
+
const kebabProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
594
|
+
return `${kebabProp}: ${value};`;
|
|
595
|
+
})
|
|
596
|
+
.join('\n ');
|
|
597
|
+
|
|
598
|
+
if (sourceCode.includes(':host {')) {
|
|
599
|
+
sourceCode = sourceCode.replace(
|
|
600
|
+
/(:host\s*\{[^}]*)(})/,
|
|
601
|
+
`$1\n ${cssProperties}\n $2`
|
|
602
|
+
);
|
|
603
|
+
} else if (sourceCode.includes('<style>')) {
|
|
604
|
+
sourceCode = sourceCode.replace(
|
|
605
|
+
/(<style>)/,
|
|
606
|
+
`$1\n :host {\n ${cssProperties}\n }`
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
fs.writeFileSync(fullPath, sourceCode, 'utf-8');
|
|
612
|
+
|
|
613
|
+
console.log(`[DevTools] Saved global changes for <${tagName}>`);
|
|
614
|
+
notifyHMRClients(fullPath);
|
|
615
|
+
return { success: true, message: 'Global changes saved successfully' };
|
|
616
|
+
|
|
617
|
+
} catch (error) {
|
|
618
|
+
console.error('[DevTools] Error saving global changes:', error);
|
|
619
|
+
return { success: false, message: error.message };
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Handle API routes
|
|
624
|
+
async function handleApiRoute(req, res) {
|
|
625
|
+
const url = req.url;
|
|
626
|
+
const method = req.method;
|
|
627
|
+
|
|
628
|
+
// CORS headers
|
|
629
|
+
|
|
630
|
+
async function proxyRemoteLogin(body) {
|
|
631
|
+
const response = await fetch(DEV_REMOTE_AUTH_LOGIN_URL, {
|
|
632
|
+
method: 'POST',
|
|
633
|
+
headers: {
|
|
634
|
+
'Content-Type': 'application/json',
|
|
635
|
+
},
|
|
636
|
+
body: JSON.stringify(body),
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
const contentType = response.headers.get('content-type') || 'application/json';
|
|
640
|
+
const data = contentType.includes('application/json')
|
|
641
|
+
? await response.json()
|
|
642
|
+
: { message: await response.text() };
|
|
643
|
+
|
|
644
|
+
return {
|
|
645
|
+
status: response.status,
|
|
646
|
+
data,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
650
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
651
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
652
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
653
|
+
|
|
654
|
+
if (method === 'OPTIONS') {
|
|
655
|
+
res.writeHead(200);
|
|
656
|
+
res.end();
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
try {
|
|
661
|
+
// POST /api/auth/login
|
|
662
|
+
if (url === '/api/auth/login' && method === 'POST') {
|
|
663
|
+
const body = await parseBody(req);
|
|
664
|
+
let result;
|
|
665
|
+
|
|
666
|
+
if (DEV_REMOTE_AUTH_LOGIN_URL) {
|
|
667
|
+
try {
|
|
668
|
+
result = await proxyRemoteLogin(body);
|
|
669
|
+
} catch (error) {
|
|
670
|
+
console.error('[API] Remote login proxy failed, falling back to mock login:', error.message);
|
|
671
|
+
result = mockApi.handleLogin(body);
|
|
672
|
+
}
|
|
673
|
+
} else {
|
|
674
|
+
result = mockApi.handleLogin(body);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
res.writeHead(result.status, { 'Content-Type': 'application/json' });
|
|
678
|
+
res.end(JSON.stringify(result.data));
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// GET /api/dashboard/stats
|
|
683
|
+
if (url === '/api/dashboard/stats' && method === 'GET') {
|
|
684
|
+
const authHeader = req.headers.authorization;
|
|
685
|
+
const user = mockApi.verifyToken(authHeader);
|
|
686
|
+
|
|
687
|
+
if (!user) {
|
|
688
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
689
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const result = mockApi.handleDashboard();
|
|
694
|
+
res.writeHead(result.status, { 'Content-Type': 'application/json' });
|
|
695
|
+
res.end(JSON.stringify(result.data));
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// GET /api/auth/verify
|
|
700
|
+
if (url === '/api/auth/verify' && method === 'GET') {
|
|
701
|
+
const authHeader = req.headers.authorization;
|
|
702
|
+
const user = mockApi.verifyToken(authHeader);
|
|
703
|
+
|
|
704
|
+
if (!user) {
|
|
705
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
706
|
+
res.end(JSON.stringify({ error: 'Unauthorized - please login again' }));
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const result = mockApi.handleVerify(user);
|
|
711
|
+
res.writeHead(result.status, { 'Content-Type': 'application/json' });
|
|
712
|
+
res.end(JSON.stringify(result.data));
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// GET /api/users/:id
|
|
717
|
+
if (url.startsWith('/api/users/') && method === 'GET') {
|
|
718
|
+
const authHeader = req.headers.authorization;
|
|
719
|
+
const user = mockApi.verifyToken(authHeader);
|
|
720
|
+
|
|
721
|
+
if (!user) {
|
|
722
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
723
|
+
res.end(JSON.stringify({ error: 'Unauthorized - please login again' }));
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const userId = url.replace('/api/users/', '').split('?')[0];
|
|
728
|
+
const result = mockApi.handleUserDetail(userId);
|
|
729
|
+
res.writeHead(result.status, { 'Content-Type': 'application/json' });
|
|
730
|
+
res.end(JSON.stringify(result.data));
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// ============================================
|
|
735
|
+
// DEV TOOLS API (only works on localhost)
|
|
736
|
+
// These endpoints allow live editing of components
|
|
737
|
+
// ============================================
|
|
738
|
+
|
|
739
|
+
// GET /api/dev/component/:tagName - Get component metadata
|
|
740
|
+
if (url.startsWith('/api/dev/component/') && method === 'GET' && !url.includes('/edit')) {
|
|
741
|
+
const tagName = url.replace('/api/dev/component/', '');
|
|
742
|
+
const metadata = await getComponentMetadata(tagName);
|
|
743
|
+
|
|
744
|
+
if (!metadata) {
|
|
745
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
746
|
+
res.end(JSON.stringify({ error: `Component <${tagName}> not found` }));
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
751
|
+
res.end(JSON.stringify(metadata));
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// POST /api/dev/component/edit - Edit component file
|
|
756
|
+
if (url === '/api/dev/component/edit' && method === 'POST') {
|
|
757
|
+
const body = await parseBody(req);
|
|
758
|
+
const result = await editComponentFile(body);
|
|
759
|
+
|
|
760
|
+
res.writeHead(result.success ? 200 : 400, { 'Content-Type': 'application/json' });
|
|
761
|
+
res.end(JSON.stringify(result));
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// POST /api/dev/component/save-instance - Save instance changes to HTML
|
|
766
|
+
if (url === '/api/dev/component/save-instance' && method === 'POST') {
|
|
767
|
+
const body = await parseBody(req);
|
|
768
|
+
const result = await saveInstanceChanges(body);
|
|
769
|
+
|
|
770
|
+
res.writeHead(result.success ? 200 : 400, { 'Content-Type': 'application/json' });
|
|
771
|
+
res.end(JSON.stringify(result));
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// POST /api/dev/component/save-global - Save global changes to component file
|
|
776
|
+
if (url === '/api/dev/component/save-global' && method === 'POST') {
|
|
777
|
+
const body = await parseBody(req);
|
|
778
|
+
const result = await saveGlobalChanges(body);
|
|
779
|
+
|
|
780
|
+
res.writeHead(result.success ? 200 : 400, { 'Content-Type': 'application/json' });
|
|
781
|
+
res.end(JSON.stringify(result));
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// POST /api/dev/component/delete-instance - Delete component instance from HTML
|
|
786
|
+
if (url === '/api/dev/component/delete-instance' && method === 'POST') {
|
|
787
|
+
const body = await parseBody(req);
|
|
788
|
+
const { tagName, htmlPath, outerHTML } = body;
|
|
789
|
+
const fullPath = path.join(ROOT_DIR, htmlPath);
|
|
790
|
+
|
|
791
|
+
if (!fs.existsSync(fullPath)) {
|
|
792
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
793
|
+
res.end(JSON.stringify({ success: false, error: 'HTML file not found' }));
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
let htmlContent = fs.readFileSync(fullPath, 'utf-8');
|
|
798
|
+
|
|
799
|
+
// Remove the exact instance (including attributes and content)
|
|
800
|
+
const escapedHTML = outerHTML.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
801
|
+
const regex = new RegExp(escapedHTML, 'g');
|
|
802
|
+
|
|
803
|
+
if (!htmlContent.match(regex)) {
|
|
804
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
805
|
+
res.end(JSON.stringify({ success: false, error: 'Component instance not found in HTML' }));
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
htmlContent = htmlContent.replace(regex, '');
|
|
810
|
+
fs.writeFileSync(fullPath, htmlContent, 'utf-8');
|
|
811
|
+
|
|
812
|
+
console.log(`[DevTools] Deleted <${tagName}> instance from ${htmlPath}`);
|
|
813
|
+
|
|
814
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
815
|
+
res.end(JSON.stringify({ success: true }));
|
|
816
|
+
|
|
817
|
+
// Trigger HMR
|
|
818
|
+
notifyHMRClients(htmlPath);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// API route not found
|
|
823
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
824
|
+
res.end(JSON.stringify({ error: 'API endpoint not found' }));
|
|
825
|
+
|
|
826
|
+
} catch (error) {
|
|
827
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
828
|
+
res.end(JSON.stringify({ error: 'Server error: ' + error.message }));
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const server = http.createServer(async (req, res) => {
|
|
833
|
+
// Handle API routes
|
|
834
|
+
if (req.url.startsWith('/api/')) {
|
|
835
|
+
await handleApiRoute(req, res);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Strip query parameters for file path resolution
|
|
840
|
+
const urlWithoutQuery = req.url.split('?')[0];
|
|
841
|
+
|
|
842
|
+
// Handle static files and SPA routing
|
|
843
|
+
let filePath = path.join(ROOT_DIR, urlWithoutQuery === '/' ? 'index.html' : urlWithoutQuery);
|
|
844
|
+
const pathExists = fs.existsSync(filePath);
|
|
845
|
+
const pathIsDirectory = pathExists ? fs.statSync(filePath).isDirectory() : false;
|
|
846
|
+
|
|
847
|
+
// Handle favicon - return 204 if not found to avoid errors
|
|
848
|
+
if (urlWithoutQuery === '/favicon.ico' && !fs.existsSync(filePath)) {
|
|
849
|
+
res.writeHead(204); // No Content
|
|
850
|
+
res.end();
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Determine if this is a file request or a route request
|
|
855
|
+
const ext = path.extname(urlWithoutQuery);
|
|
856
|
+
const isFileRequest = ext && ext !== '';
|
|
857
|
+
|
|
858
|
+
// Check if file exists (only for actual file requests with extensions)
|
|
859
|
+
if (isFileRequest && !pathExists) {
|
|
860
|
+
// File request but file doesn't exist - return 404
|
|
861
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
862
|
+
res.end('File not found: ' + urlWithoutQuery);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// If file doesn't exist and no extension, serve index.html for SPA routing
|
|
867
|
+
if (!isFileRequest && (!pathExists || pathIsDirectory)) {
|
|
868
|
+
filePath = path.join(ROOT_DIR, 'index.html');
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Get file extension
|
|
872
|
+
const fileExt = path.extname(filePath);
|
|
873
|
+
const contentType = MIME_TYPES[fileExt] || 'text/plain';
|
|
874
|
+
|
|
875
|
+
// Read and serve file
|
|
876
|
+
fs.readFile(filePath, (error, content) => {
|
|
877
|
+
if (error) {
|
|
878
|
+
res.writeHead(500);
|
|
879
|
+
res.end('Server Error: ' + error.code);
|
|
880
|
+
} else {
|
|
881
|
+
// Add headers
|
|
882
|
+
const headers = { 'Content-Type': contentType };
|
|
883
|
+
|
|
884
|
+
// In development, disable all caching for instant updates
|
|
885
|
+
const isDevelopment = process.env.NODE_ENV !== 'production';
|
|
886
|
+
|
|
887
|
+
// In development, set a permissive CSP to allow HMR/devtools eval
|
|
888
|
+
if (isDevelopment && contentType === 'text/html') {
|
|
889
|
+
const connectSrc = [
|
|
890
|
+
"'self'",
|
|
891
|
+
'ws://localhost:8001',
|
|
892
|
+
DEV_REMOTE_API_ORIGIN,
|
|
893
|
+
].join(' ');
|
|
894
|
+
|
|
895
|
+
headers['Content-Security-Policy'] = [
|
|
896
|
+
"default-src 'self'",
|
|
897
|
+
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
|
|
898
|
+
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
|
899
|
+
"font-src 'self' https://fonts.gstatic.com",
|
|
900
|
+
`connect-src ${connectSrc}`,
|
|
901
|
+
"img-src 'self' data:"
|
|
902
|
+
].join('; ');
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
if (isDevelopment) {
|
|
906
|
+
// No caching in development for HMR
|
|
907
|
+
headers['Cache-Control'] = 'no-cache, no-store, must-revalidate';
|
|
908
|
+
headers['Pragma'] = 'no-cache';
|
|
909
|
+
headers['Expires'] = '0';
|
|
910
|
+
} else {
|
|
911
|
+
// Production caching (with cache busting in place)
|
|
912
|
+
if (['.css', '.js'].includes(fileExt)) {
|
|
913
|
+
// Cache CSS/JS for 1 day with cache busting
|
|
914
|
+
headers['Cache-Control'] = 'public, max-age=86400';
|
|
915
|
+
} else if (['.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.woff', '.woff2'].includes(fileExt)) {
|
|
916
|
+
// Cache images and fonts for 30 days
|
|
917
|
+
headers['Cache-Control'] = 'public, max-age=2592000';
|
|
918
|
+
} else if (fileExt === '.html') {
|
|
919
|
+
// HTML should not be cached
|
|
920
|
+
headers['Cache-Control'] = 'no-cache, no-store, must-revalidate';
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
res.writeHead(200, headers);
|
|
925
|
+
res.end(content, 'utf-8');
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
server.listen(PORT, () => {
|
|
931
|
+
console.log(`Server running at http://localhost:${PORT}/`);
|
|
932
|
+
console.log(`Serving files from: ${ROOT_DIR}`);
|
|
933
|
+
console.log(`SPA mode: All routes fallback to index.html`);
|
|
934
|
+
console.log(`Mock API: /api/* endpoints available`);
|
|
935
|
+
console.log(`\nš Test credentials:`);
|
|
936
|
+
console.log(` Email: demo@example.com`);
|
|
937
|
+
console.log(` Password: pa$$w0rd\n`);
|
|
938
|
+
console.log(DEV_REMOTE_AUTH_LOGIN_URL
|
|
939
|
+
? `Remote auth proxy enabled: ${DEV_REMOTE_AUTH_LOGIN_URL}`
|
|
940
|
+
: 'Remote auth proxy disabled: using local mock auth');
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
// ========== Hot Module Replacement (HMR) ==========
|
|
944
|
+
|
|
945
|
+
const wss = new WebSocketServer({ port: HMR_PORT });
|
|
946
|
+
const hmrClients = new Set();
|
|
947
|
+
|
|
948
|
+
function notifyHMRClients(file = 'unknown') {
|
|
949
|
+
const message = JSON.stringify({ type: 'file-changed', file, timestamp: Date.now() });
|
|
950
|
+
hmrClients.forEach(client => {
|
|
951
|
+
if (client.readyState === 1) client.send(message);
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// Track connected HMR clients
|
|
956
|
+
wss.on('connection', (ws) => {
|
|
957
|
+
hmrClients.add(ws);
|
|
958
|
+
console.log('š„ HMR client connected');
|
|
959
|
+
|
|
960
|
+
ws.on('close', () => {
|
|
961
|
+
hmrClients.delete(ws);
|
|
962
|
+
console.log('š„ HMR client disconnected');
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
ws.on('error', (error) => {
|
|
966
|
+
console.error('š„ HMR WebSocket error:', error.message);
|
|
967
|
+
});
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
console.log(`š„ HMR enabled on ws://localhost:${HMR_PORT}`);
|
|
971
|
+
|
|
972
|
+
// āā File Watchers āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
973
|
+
//
|
|
974
|
+
// Strategy: the server NEVER calls tsc. Instead:
|
|
975
|
+
// - dist/ is watched for .js output written by the external `tsc --watch` process
|
|
976
|
+
// - src/ is watched for .css and .html changes (no compilation needed)
|
|
977
|
+
// - index.html in root is watched directly
|
|
978
|
+
//
|
|
979
|
+
// To get fast HMR, run the TypeScript compiler in watch mode in a separate
|
|
980
|
+
// terminal alongside the server:
|
|
981
|
+
//
|
|
982
|
+
// Terminal 1: npm start (this server)
|
|
983
|
+
// Terminal 2: npx tsc --watch (incremental compiler)
|
|
984
|
+
// Terminal 3 (optional): npx tsc-alias --watch
|
|
985
|
+
//
|
|
986
|
+
// The compiler picks up a save, incrementally rebuilds in ~100-400ms, writes
|
|
987
|
+
// the .js file to dist/, and the server immediately fires the HMR WebSocket
|
|
988
|
+
// message ā no cold tsc spawn, no npx overhead.
|
|
989
|
+
|
|
990
|
+
const distDir = path.join(ROOT_DIR, 'dist');
|
|
991
|
+
const srcDir = path.join(ROOT_DIR, 'src');
|
|
992
|
+
|
|
993
|
+
function notifyFile(file) {
|
|
994
|
+
const message = JSON.stringify({ type: 'file-changed', file, timestamp: Date.now() });
|
|
995
|
+
hmrClients.forEach(client => {
|
|
996
|
+
if (client.readyState === 1) client.send(message);
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function debounce(fn, delay) {
|
|
1001
|
+
let timer = null;
|
|
1002
|
+
return (...args) => {
|
|
1003
|
+
clearTimeout(timer);
|
|
1004
|
+
timer = setTimeout(() => fn(...args), delay);
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
try {
|
|
1009
|
+
// Watch dist/ ā fires after tsc --watch writes compiled .js output.
|
|
1010
|
+
// tsc writes several files per compile (foo.js, foo.js.map, foo.d.ts).
|
|
1011
|
+
// We track the last .js file seen in the debounce window so the callback
|
|
1012
|
+
// always fires with a real JS filename even if the final fs event was a
|
|
1013
|
+
// .map or .d.ts file.
|
|
1014
|
+
let pendingJsFile = null;
|
|
1015
|
+
let distDebounceTimer = null;
|
|
1016
|
+
|
|
1017
|
+
fs.watch(distDir, { recursive: true }, (eventType, filename) => {
|
|
1018
|
+
if (!filename) return;
|
|
1019
|
+
const norm = filename.replace(/\\/g, '/');
|
|
1020
|
+
if (norm.endsWith('.js') && !norm.endsWith('.d.ts')) {
|
|
1021
|
+
pendingJsFile = norm;
|
|
1022
|
+
}
|
|
1023
|
+
clearTimeout(distDebounceTimer);
|
|
1024
|
+
distDebounceTimer = setTimeout(() => {
|
|
1025
|
+
if (pendingJsFile) {
|
|
1026
|
+
console.log(`[HMR] dist changed: ${pendingJsFile}`);
|
|
1027
|
+
notifyFile(pendingJsFile);
|
|
1028
|
+
pendingJsFile = null;
|
|
1029
|
+
}
|
|
1030
|
+
}, 50);
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
// Watch src/ ā CSS and HTML only (TS is handled via dist/ above)
|
|
1034
|
+
let pendingSrcFile = null;
|
|
1035
|
+
let srcDebounceTimer = null;
|
|
1036
|
+
|
|
1037
|
+
fs.watch(srcDir, { recursive: true }, (eventType, filename) => {
|
|
1038
|
+
if (!filename) return;
|
|
1039
|
+
const norm = filename.replace(/\\/g, '/');
|
|
1040
|
+
if (norm.endsWith('.css') || norm.endsWith('.html')) {
|
|
1041
|
+
pendingSrcFile = norm;
|
|
1042
|
+
}
|
|
1043
|
+
clearTimeout(srcDebounceTimer);
|
|
1044
|
+
srcDebounceTimer = setTimeout(() => {
|
|
1045
|
+
if (pendingSrcFile) {
|
|
1046
|
+
console.log(`[HMR] src changed: ${pendingSrcFile}`);
|
|
1047
|
+
notifyFile(pendingSrcFile);
|
|
1048
|
+
pendingSrcFile = null;
|
|
1049
|
+
}
|
|
1050
|
+
}, 50);
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
// Watch the root shell HTML file
|
|
1054
|
+
for (const shellFile of ['index.html']) {
|
|
1055
|
+
fs.watch(path.join(ROOT_DIR, shellFile), debounce(() => {
|
|
1056
|
+
console.log(`[HMR] shell changed: ${shellFile}`);
|
|
1057
|
+
notifyFile(shellFile);
|
|
1058
|
+
}, 50));
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
console.log('[HMR] Watching dist/ for compiled JS output');
|
|
1062
|
+
console.log('[HMR] Watching src/ for CSS and HTML changes');
|
|
1063
|
+
console.log('[HMR] NOTE: Run "npx tsc --watch" in a separate terminal for instant TS recompilation');
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
console.error('Could not start file watcher:', error.message);
|
|
1066
|
+
}
|