elit 3.0.2 → 3.0.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elit",
3
- "version": "3.0.2",
3
+ "version": "3.0.3",
4
4
  "description": "Optimized lightweight library for creating DOM elements with reactive state",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
package/src/server.ts CHANGED
@@ -129,7 +129,7 @@ const send403 = (res: ServerResponse, msg = 'Forbidden'): void => sendError(res,
129
129
  const send500 = (res: ServerResponse, msg = 'Internal Server Error'): void => sendError(res, 500, msg);
130
130
 
131
131
  // Import map for all Elit client-side modules (reused in serveFile and serveSSR)
132
- const createElitImportMap = (basePath: string = '', mode: 'dev' | 'preview' = 'dev'): string => {
132
+ const createElitImportMap = async (rootDir: string, basePath: string = '', mode: 'dev' | 'preview' = 'dev'): Promise<string> => {
133
133
  // In dev mode, use built files from node_modules/elit/dist
134
134
  // In preview mode, use built files from dist
135
135
  const srcPath = mode === 'dev'
@@ -138,19 +138,26 @@ const createElitImportMap = (basePath: string = '', mode: 'dev' | 'preview' = 'd
138
138
 
139
139
  const fileExt = mode === 'dev' ? '.ts' : '.mjs';
140
140
 
141
- return `<script type="importmap">{
142
- "imports": {
143
- "elit": "${srcPath}/index${fileExt}",
144
- "elit/": "${srcPath}/",
145
- "elit/dom": "${srcPath}/dom${fileExt}",
146
- "elit/state": "${srcPath}/state${fileExt}",
147
- "elit/style": "${srcPath}/style${fileExt}",
148
- "elit/el": "${srcPath}/el${fileExt}",
149
- "elit/router": "${srcPath}/router${fileExt}",
150
- "elit/hmr": "${srcPath}/hmr${fileExt}",
151
- "elit/types": "${srcPath}/types${fileExt}"
152
- }
153
- }</script>`;
141
+ // Base Elit imports
142
+ const elitImports: ImportMapEntry = {
143
+ "elit": `${srcPath}/index${fileExt}`,
144
+ "elit/": `${srcPath}/`,
145
+ "elit/dom": `${srcPath}/dom${fileExt}`,
146
+ "elit/state": `${srcPath}/state${fileExt}`,
147
+ "elit/style": `${srcPath}/style${fileExt}`,
148
+ "elit/el": `${srcPath}/el${fileExt}`,
149
+ "elit/router": `${srcPath}/router${fileExt}`,
150
+ "elit/hmr": `${srcPath}/hmr${fileExt}`,
151
+ "elit/types": `${srcPath}/types${fileExt}`
152
+ };
153
+
154
+ // Generate external library imports
155
+ const externalImports = await generateExternalImportMaps(rootDir, basePath);
156
+
157
+ // Merge imports (Elit imports take precedence)
158
+ const allImports = { ...externalImports, ...elitImports };
159
+
160
+ return `<script type="importmap">${JSON.stringify({ imports: allImports }, null, 2)}</script>`;
154
161
  };
155
162
 
156
163
  // Helper function to generate HMR script (reused in serveFile and serveSSR)
@@ -193,6 +200,299 @@ async function findSpecialDir(startDir: string, targetDir: string): Promise<stri
193
200
  return null;
194
201
  }
195
202
 
203
+ // ===== External Library Import Maps =====
204
+
205
+ interface PackageExports {
206
+ [key: string]: string | PackageExports;
207
+ }
208
+
209
+ interface PackageJson {
210
+ name?: string;
211
+ main?: string;
212
+ module?: string;
213
+ browser?: string | Record<string, string | false>;
214
+ exports?: string | PackageExports | { [key: string]: any };
215
+ type?: 'module' | 'commonjs';
216
+ sideEffects?: boolean | string[];
217
+ }
218
+
219
+ interface ImportMapEntry {
220
+ [importName: string]: string;
221
+ }
222
+
223
+ // Cache for generated import maps to avoid re-scanning
224
+ const importMapCache = new Map<string, ImportMapEntry>();
225
+
226
+ /**
227
+ * Clear import map cache (useful when packages are added/removed)
228
+ */
229
+ export function clearImportMapCache(): void {
230
+ importMapCache.clear();
231
+ }
232
+
233
+ /**
234
+ * Scan node_modules and generate import maps for external libraries
235
+ */
236
+ async function generateExternalImportMaps(rootDir: string, basePath: string = ''): Promise<ImportMapEntry> {
237
+ const cacheKey = `${rootDir}:${basePath}`;
238
+ if (importMapCache.has(cacheKey)) {
239
+ return importMapCache.get(cacheKey)!;
240
+ }
241
+
242
+ const importMap: ImportMapEntry = {};
243
+ const nodeModulesPath = await findNodeModules(rootDir);
244
+
245
+ if (!nodeModulesPath) {
246
+ importMapCache.set(cacheKey, importMap);
247
+ return importMap;
248
+ }
249
+
250
+ try {
251
+ const { readdir } = await import('./fs');
252
+ const packages = await readdir(nodeModulesPath);
253
+
254
+ for (const pkgEntry of packages) {
255
+ // Convert Dirent to string
256
+ const pkg = typeof pkgEntry === 'string' ? pkgEntry : pkgEntry.name;
257
+
258
+ // Skip special directories
259
+ if (pkg.startsWith('.')) continue;
260
+
261
+ // Handle scoped packages (@org/package)
262
+ if (pkg.startsWith('@')) {
263
+ try {
264
+ const scopedPackages = await readdir(join(nodeModulesPath, pkg));
265
+ for (const scopedEntry of scopedPackages) {
266
+ const scopedPkg = typeof scopedEntry === 'string' ? scopedEntry : scopedEntry.name;
267
+ const fullPkgName = `${pkg}/${scopedPkg}`;
268
+ await processPackage(nodeModulesPath, fullPkgName, importMap, basePath);
269
+ }
270
+ } catch {
271
+ // Skip if can't read scoped directory
272
+ }
273
+ } else {
274
+ await processPackage(nodeModulesPath, pkg, importMap, basePath);
275
+ }
276
+ }
277
+ } catch (error) {
278
+ console.error('[Import Maps] Error scanning node_modules:', error);
279
+ }
280
+
281
+ importMapCache.set(cacheKey, importMap);
282
+ return importMap;
283
+ }
284
+
285
+ /**
286
+ * Find node_modules directory by walking up the directory tree
287
+ */
288
+ async function findNodeModules(startDir: string): Promise<string | null> {
289
+ const foundDir = await findSpecialDir(startDir, 'node_modules');
290
+ return foundDir ? join(foundDir, 'node_modules') : null;
291
+ }
292
+
293
+ /**
294
+ * Check if a package is browser-compatible
295
+ */
296
+ function isBrowserCompatible(pkgName: string, pkgJson: PackageJson): boolean {
297
+ // Skip build tools, compilers, and Node.js-only packages
298
+ const buildTools = [
299
+ 'typescript', 'esbuild', '@esbuild/',
300
+ 'tsx', 'tsup', 'rollup', 'vite', 'webpack', 'parcel',
301
+ 'terser', 'uglify', 'babel', '@babel/',
302
+ 'postcss', 'autoprefixer', 'cssnano',
303
+ 'sass', 'less', 'stylus'
304
+ ];
305
+
306
+ const nodeOnly = [
307
+ 'node-', '@node-', 'fsevents', 'chokidar',
308
+ 'express', 'koa', 'fastify', 'nest',
309
+ 'commander', 'yargs', 'inquirer', 'chalk', 'ora',
310
+ 'nodemon', 'pm2', 'dotenv'
311
+ ];
312
+
313
+ const testingTools = [
314
+ 'jest', 'vitest', 'mocha', 'chai', 'jasmine',
315
+ '@jest/', '@testing-library/', '@vitest/',
316
+ 'playwright', 'puppeteer', 'cypress'
317
+ ];
318
+
319
+ const linters = [
320
+ 'eslint', '@eslint/', 'prettier', 'tslint',
321
+ 'stylelint', 'commitlint'
322
+ ];
323
+
324
+ const typeDefinitions = [
325
+ '@types/', '@typescript-eslint/'
326
+ ];
327
+
328
+ const utilities = [
329
+ 'get-tsconfig', 'resolve-pkg-maps', 'pkg-types',
330
+ 'fast-glob', 'globby', 'micromatch',
331
+ 'execa', 'cross-spawn', 'shelljs'
332
+ ];
333
+
334
+ // Combine all skip lists
335
+ const skipPatterns = [
336
+ ...buildTools,
337
+ ...nodeOnly,
338
+ ...testingTools,
339
+ ...linters,
340
+ ...typeDefinitions,
341
+ ...utilities
342
+ ];
343
+
344
+ // Check if package name matches skip patterns
345
+ if (skipPatterns.some(pattern => pkgName.startsWith(pattern))) {
346
+ return false;
347
+ }
348
+
349
+ // Skip CommonJS-only lodash (prefer lodash-es)
350
+ if (pkgName === 'lodash') {
351
+ return false;
352
+ }
353
+
354
+ // Prefer packages with explicit browser field or module field (ESM)
355
+ if (pkgJson.browser || pkgJson.module) {
356
+ return true;
357
+ }
358
+
359
+ // Prefer packages with exports field that includes "import" or "browser"
360
+ if (pkgJson.exports) {
361
+ const exportsStr = JSON.stringify(pkgJson.exports);
362
+ if (exportsStr.includes('"import"') || exportsStr.includes('"browser"')) {
363
+ return true;
364
+ }
365
+ }
366
+
367
+ // Skip packages that are explicitly marked as type: "commonjs" without module/browser fields
368
+ if (pkgJson.type === 'commonjs' && !pkgJson.module && !pkgJson.browser) {
369
+ return false;
370
+ }
371
+
372
+ // Default: allow if it has exports or is type: "module"
373
+ return !!(pkgJson.exports || pkgJson.type === 'module' || pkgJson.module);
374
+ }
375
+
376
+ /**
377
+ * Process a single package and add its exports to the import map
378
+ */
379
+ async function processPackage(
380
+ nodeModulesPath: string,
381
+ pkgName: string,
382
+ importMap: ImportMapEntry,
383
+ basePath: string
384
+ ): Promise<void> {
385
+ const pkgPath = join(nodeModulesPath, pkgName);
386
+ const pkgJsonPath = join(pkgPath, 'package.json');
387
+
388
+ try {
389
+ const pkgJsonContent = await readFile(pkgJsonPath);
390
+ const pkgJson: PackageJson = JSON.parse(pkgJsonContent.toString());
391
+
392
+ // Check if package is browser-compatible
393
+ if (!isBrowserCompatible(pkgName, pkgJson)) {
394
+ return;
395
+ }
396
+
397
+ const baseUrl = basePath ? `${basePath}/node_modules/${pkgName}` : `/node_modules/${pkgName}`;
398
+
399
+ // Handle exports field (modern)
400
+ if (pkgJson.exports) {
401
+ processExportsField(pkgName, pkgJson.exports, baseUrl, importMap);
402
+ }
403
+ // Fallback to main/module/browser fields (legacy)
404
+ else {
405
+ const entryPoint = pkgJson.browser || pkgJson.module || pkgJson.main || 'index.js';
406
+ importMap[pkgName] = `${baseUrl}/${entryPoint}`;
407
+
408
+ // Add trailing slash for subpath imports
409
+ importMap[`${pkgName}/`] = `${baseUrl}/`;
410
+ }
411
+ } catch {
412
+ // Skip packages without package.json or invalid JSON
413
+ }
414
+ }
415
+
416
+ /**
417
+ * Process package.json exports field and add to import map
418
+ */
419
+ function processExportsField(
420
+ pkgName: string,
421
+ exports: string | PackageExports | { [key: string]: any },
422
+ baseUrl: string,
423
+ importMap: ImportMapEntry
424
+ ): void {
425
+ // Simple string export
426
+ if (typeof exports === 'string') {
427
+ importMap[pkgName] = `${baseUrl}/${exports}`;
428
+ importMap[`${pkgName}/`] = `${baseUrl}/`;
429
+ return;
430
+ }
431
+
432
+ // Object exports
433
+ if (typeof exports === 'object' && exports !== null) {
434
+ // Handle "." export (main entry)
435
+ if ('.' in exports) {
436
+ const dotExport = exports['.'];
437
+ const resolved = resolveExport(dotExport);
438
+ if (resolved) {
439
+ importMap[pkgName] = `${baseUrl}/${resolved}`;
440
+ }
441
+ } else if ('import' in exports) {
442
+ // Root-level import/require
443
+ const resolved = resolveExport(exports);
444
+ if (resolved) {
445
+ importMap[pkgName] = `${baseUrl}/${resolved}`;
446
+ }
447
+ }
448
+
449
+ // Handle subpath exports
450
+ for (const [key, value] of Object.entries(exports)) {
451
+ if (key === '.' || key === 'import' || key === 'require' || key === 'types' || key === 'default') {
452
+ continue;
453
+ }
454
+
455
+ const resolved = resolveExport(value);
456
+ if (resolved) {
457
+ // Remove leading ./ from key
458
+ const cleanKey = key.startsWith('./') ? key.slice(2) : key;
459
+ const importName = cleanKey ? `${pkgName}/${cleanKey}` : pkgName;
460
+ importMap[importName] = `${baseUrl}/${resolved}`;
461
+ }
462
+ }
463
+
464
+ // Always add trailing slash for subpath imports
465
+ importMap[`${pkgName}/`] = `${baseUrl}/`;
466
+ }
467
+ }
468
+
469
+ /**
470
+ * Resolve export value to actual file path
471
+ * Handles conditional exports (import/require/default)
472
+ */
473
+ function resolveExport(exportValue: any): string | null {
474
+ if (typeof exportValue === 'string') {
475
+ // Remove leading ./
476
+ return exportValue.startsWith('./') ? exportValue.slice(2) : exportValue;
477
+ }
478
+
479
+ if (typeof exportValue === 'object' && exportValue !== null) {
480
+ // Prefer import over require over default
481
+ const resolved = exportValue.import || exportValue.browser || exportValue.default || exportValue.require;
482
+
483
+ // Handle nested objects recursively (e.g., TypeScript's complex exports)
484
+ if (typeof resolved === 'object' && resolved !== null) {
485
+ return resolveExport(resolved);
486
+ }
487
+
488
+ if (typeof resolved === 'string') {
489
+ return resolved.startsWith('./') ? resolved.slice(2) : resolved;
490
+ }
491
+ }
492
+
493
+ return null;
494
+ }
495
+
196
496
  // ===== Middleware =====
197
497
 
198
498
  export function cors(options: {
@@ -694,7 +994,7 @@ export function createDevServer(options: DevServerOptions): DevServer {
694
994
  let filePath: string;
695
995
  if (url === '/' && matchedClient.ssr && !matchedClient.index) {
696
996
  // Use SSR directly when configured and no custom index specified
697
- return serveSSR(res, matchedClient);
997
+ return await serveSSR(res, matchedClient);
698
998
  } else {
699
999
  // Use custom index file if specified, otherwise default to /index.html
700
1000
  filePath = url === '/' ? (matchedClient.index || '/index.html') : url;
@@ -809,7 +1109,7 @@ export function createDevServer(options: DevServerOptions): DevServer {
809
1109
  if (!res.headersSent) {
810
1110
  // If index.html not found but SSR function exists, use SSR
811
1111
  if (filePath === '/index.html' && matchedClient.ssr) {
812
- return serveSSR(res, matchedClient);
1112
+ return await serveSSR(res, matchedClient);
813
1113
  }
814
1114
  if (config.logging) console.log(`[404] ${filePath}`);
815
1115
  return send404(res, '404 Not Found');
@@ -840,7 +1140,7 @@ export function createDevServer(options: DevServerOptions): DevServer {
840
1140
  if (config.logging) console.log(`[DEBUG] No index file found in directory`);
841
1141
  // If index.html not found in directory but SSR function exists, use SSR
842
1142
  if (matchedClient.ssr) {
843
- return serveSSR(res, matchedClient);
1143
+ return await serveSSR(res, matchedClient);
844
1144
  }
845
1145
  return send404(res, '404 Not Found');
846
1146
  }
@@ -866,13 +1166,13 @@ export function createDevServer(options: DevServerOptions): DevServer {
866
1166
  return send403(res, '403 Forbidden');
867
1167
  }
868
1168
  await stat(indexPath);
869
- return serveFile(indexPath, res, matchedClient);
1169
+ return serveFile(indexPath, res, matchedClient, isDistRequest || isNodeModulesRequest);
870
1170
  } catch {
871
1171
  return send404(res, '404 Not Found');
872
1172
  }
873
1173
  }
874
1174
 
875
- await serveFile(fullPath, res, matchedClient);
1175
+ await serveFile(fullPath, res, matchedClient, isDistRequest || isNodeModulesRequest);
876
1176
  } catch (error) {
877
1177
  // Only send 404 if response hasn't been sent yet
878
1178
  if (!res.headersSent) {
@@ -883,22 +1183,17 @@ export function createDevServer(options: DevServerOptions): DevServer {
883
1183
  });
884
1184
 
885
1185
  // Serve file helper
886
- async function serveFile(filePath: string, res: ServerResponse, client: NormalizedClient) {
1186
+ async function serveFile(filePath: string, res: ServerResponse, client: NormalizedClient, isNodeModulesOrDist: boolean = false) {
887
1187
  try {
888
1188
  const rootDir = await realpath(resolve(client.root));
889
1189
 
890
1190
  // Security: Check path before resolving symlinks
891
1191
  const unresolvedPath = resolve(filePath);
892
- const isNodeModules = filePath.includes('/node_modules/') || filePath.includes('\\node_modules\\');
893
- const isDist = filePath.includes('/dist/') || filePath.includes('\\dist\\');
894
-
895
- // Check if path is within project root (for symlinked packages like node_modules/elit)
896
- const projectRoot = await realpath(resolve(client.root, '..'));
897
- const isInProjectRoot = unresolvedPath.startsWith(projectRoot + sep) || unresolvedPath === projectRoot;
898
1192
 
899
- if (!unresolvedPath.startsWith(rootDir + sep) && unresolvedPath !== rootDir && !isInProjectRoot) {
900
- // Allow if it's in node_modules or dist directories (these may be symlinks)
901
- if (!isNodeModules && !isDist) {
1193
+ // Skip security check for node_modules and dist (these may be symlinks)
1194
+ if (!isNodeModulesOrDist) {
1195
+ // Check if path is within project root
1196
+ if (!unresolvedPath.startsWith(rootDir + sep) && unresolvedPath !== rootDir) {
902
1197
  if (config.logging) console.log(`[403] Attempted to serve file outside allowed directories: ${filePath}`);
903
1198
  return send403(res, '403 Forbidden');
904
1199
  }
@@ -908,10 +1203,18 @@ export function createDevServer(options: DevServerOptions): DevServer {
908
1203
  let resolvedPath;
909
1204
  try {
910
1205
  resolvedPath = await realpath(unresolvedPath);
1206
+
1207
+ // For symlinked packages (like node_modules/elit), allow serving from outside rootDir
1208
+ if (isNodeModulesOrDist && resolvedPath) {
1209
+ // Allow it - this is a symlinked package
1210
+ if (config.logging && !resolvedPath.startsWith(rootDir + sep)) {
1211
+ console.log(`[DEBUG] Serving symlinked file: ${resolvedPath}`);
1212
+ }
1213
+ }
911
1214
  } catch {
912
1215
  // If index.html not found but SSR function exists, use SSR
913
1216
  if (filePath.endsWith('index.html') && client.ssr) {
914
- return serveSSR(res, client);
1217
+ return await serveSSR(res, client);
915
1218
  }
916
1219
  return send404(res, '404 Not Found');
917
1220
  }
@@ -1048,7 +1351,7 @@ export function createDevServer(options: DevServerOptions): DevServer {
1048
1351
  }
1049
1352
 
1050
1353
  // Inject import map and SSR styles into <head>
1051
- const elitImportMap = createElitImportMap(basePath, client.mode);
1354
+ const elitImportMap = await createElitImportMap(client.root, basePath, client.mode);
1052
1355
  const headInjection = ssrStyles ? `${ssrStyles}\n${elitImportMap}` : elitImportMap;
1053
1356
  html = html.includes('</head>') ? html.replace('</head>', `${headInjection}</head>`) : html;
1054
1357
  html = html.includes('</body>') ? html.replace('</body>', `${hmrScript}</body>`) : html + hmrScript;
@@ -1088,7 +1391,7 @@ export function createDevServer(options: DevServerOptions): DevServer {
1088
1391
  }
1089
1392
 
1090
1393
  // SSR helper - Generate HTML from SSR function
1091
- function serveSSR(res: ServerResponse, client: NormalizedClient) {
1394
+ async function serveSSR(res: ServerResponse, client: NormalizedClient) {
1092
1395
  try {
1093
1396
  if (!client.ssr) {
1094
1397
  return send500(res, 'SSR function not configured');
@@ -1122,7 +1425,7 @@ export function createDevServer(options: DevServerOptions): DevServer {
1122
1425
  const hmrScript = createHMRScript(config.port, basePath);
1123
1426
 
1124
1427
  // Inject import map in head, HMR script in body
1125
- const elitImportMap = createElitImportMap(basePath, client.mode);
1428
+ const elitImportMap = await createElitImportMap(client.root, basePath, client.mode);
1126
1429
  html = html.includes('</head>') ? html.replace('</head>', `${elitImportMap}</head>`) : html;
1127
1430
  html = html.includes('</body>') ? html.replace('</body>', `${hmrScript}</body>`) : html + hmrScript;
1128
1431