bertui 1.2.1 → 1.2.2

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.
@@ -1,21 +1,21 @@
1
- // bertui/src/server/dev-server-utils.js - WITH CACHE IMPORT
1
+ // bertui/src/server/dev-server-utils.js
2
2
  import { join, extname } from 'path';
3
- import { existsSync, readdirSync, watch } from 'fs';
3
+ import { existsSync, readdirSync, watch, statSync } from 'fs';
4
4
  import logger from '../logger/logger.js';
5
5
  import { compileProject } from '../client/compiler.js';
6
- import { globalCache } from '../utils/cache.js'; // ✅ Now this works!
6
+ import { globalCache } from '../utils/cache.js';
7
7
 
8
8
  // Image content type mapping
9
9
  export function getImageContentType(ext) {
10
10
  const types = {
11
- '.jpg': 'image/jpeg',
11
+ '.jpg': 'image/jpeg',
12
12
  '.jpeg': 'image/jpeg',
13
- '.png': 'image/png',
14
- '.gif': 'image/gif',
15
- '.svg': 'image/svg+xml',
13
+ '.png': 'image/png',
14
+ '.gif': 'image/gif',
15
+ '.svg': 'image/svg+xml',
16
16
  '.webp': 'image/webp',
17
17
  '.avif': 'image/avif',
18
- '.ico': 'image/x-icon'
18
+ '.ico': 'image/x-icon',
19
19
  };
20
20
  return types[ext] || 'application/octet-stream';
21
21
  }
@@ -23,278 +23,380 @@ export function getImageContentType(ext) {
23
23
  // General content type mapping
24
24
  export function getContentType(ext) {
25
25
  const types = {
26
- '.js': 'application/javascript',
27
- '.jsx': 'application/javascript',
28
- '.css': 'text/css',
26
+ '.js': 'application/javascript',
27
+ '.jsx': 'application/javascript',
28
+ '.css': 'text/css',
29
29
  '.html': 'text/html',
30
30
  '.json': 'application/json',
31
- '.png': 'image/png',
32
- '.jpg': 'image/jpeg',
31
+ '.png': 'image/png',
32
+ '.jpg': 'image/jpeg',
33
33
  '.jpeg': 'image/jpeg',
34
- '.gif': 'image/gif',
35
- '.svg': 'image/svg+xml',
34
+ '.gif': 'image/gif',
35
+ '.svg': 'image/svg+xml',
36
36
  '.webp': 'image/webp',
37
37
  '.avif': 'image/avif',
38
- '.ico': 'image/x-icon',
38
+ '.ico': 'image/x-icon',
39
39
  '.woff': 'font/woff',
40
- '.woff2': 'font/woff2',
41
- '.ttf': 'font/ttf',
42
- '.otf': 'font/otf',
43
- '.mp4': 'video/mp4',
40
+ '.woff2':'font/woff2',
41
+ '.ttf': 'font/ttf',
42
+ '.otf': 'font/otf',
43
+ '.mp4': 'video/mp4',
44
44
  '.webm': 'video/webm',
45
- '.mp3': 'audio/mpeg'
45
+ '.mp3': 'audio/mpeg',
46
46
  };
47
47
  return types[ext] || 'text/plain';
48
48
  }
49
49
 
50
- // HTML generator with caching
51
- export async function serveHTML(root, hasRouter, config, port) {
52
- const cacheKey = `html:${root}:${port}`;
53
-
54
- // Try cache first
55
- const cached = globalCache.get(cacheKey, { ttl: 1000 }); // 1 second cache during dev
56
- if (cached) {
57
- logger.debug('⚡ Serving cached HTML');
58
- return cached;
50
+ // ─────────────────────────────────────────────────────────────────────────────
51
+ // Import map builder scans node_modules at runtime so newly installed
52
+ // packages are picked up without restarting the dev server.
53
+ // ─────────────────────────────────────────────────────────────────────────────
54
+
55
+ // Cached importmap + the package.json mtime it was built from
56
+ let _cachedImportMap = null;
57
+ let _cachedPkgMtime = null;
58
+
59
+ export async function buildDevImportMap(root) {
60
+ const pkgJsonPath = join(root, 'package.json');
61
+ const nodeModulesDir = join(root, 'node_modules');
62
+
63
+ // Invalidate cache when package.json changes (new install happened)
64
+ let currentMtime = null;
65
+ try {
66
+ currentMtime = statSync(pkgJsonPath).mtimeMs;
67
+ } catch { /* package.json missing — fine */ }
68
+
69
+ if (_cachedImportMap && currentMtime === _cachedPkgMtime) {
70
+ return _cachedImportMap;
59
71
  }
60
-
72
+
73
+ logger.info('🔄 Rebuilding dev import map (new packages detected)...');
74
+
75
+ const importMap = {
76
+ 'react': 'https://esm.sh/react@18.2.0',
77
+ 'react-dom': 'https://esm.sh/react-dom@18.2.0',
78
+ 'react-dom/client': 'https://esm.sh/react-dom@18.2.0/client',
79
+ 'react/jsx-runtime': 'https://esm.sh/react@18.2.0/jsx-runtime',
80
+ };
81
+
82
+ const SKIP = new Set(['react', 'react-dom', '.bin', '.cache', '.package-lock.json', '.yarn']);
83
+
84
+ if (existsSync(nodeModulesDir)) {
85
+ try {
86
+ const packages = readdirSync(nodeModulesDir);
87
+
88
+ for (const pkg of packages) {
89
+ if (SKIP.has(pkg) || pkg.startsWith('.') || pkg.startsWith('_')) continue;
90
+
91
+ // Handle scoped packages (@org/pkg)
92
+ let pkgNames = [pkg];
93
+ if (pkg.startsWith('@')) {
94
+ const scopeDir = join(nodeModulesDir, pkg);
95
+ try {
96
+ if (statSync(scopeDir).isDirectory()) {
97
+ pkgNames = readdirSync(scopeDir).map(sub => `${pkg}/${sub}`);
98
+ }
99
+ } catch { continue; }
100
+ }
101
+
102
+ for (const pkgName of pkgNames) {
103
+ const pkgDir = join(nodeModulesDir, pkgName);
104
+ const pkgJsonFile = join(pkgDir, 'package.json');
105
+
106
+ try {
107
+ if (!statSync(pkgDir).isDirectory()) continue;
108
+ } catch { continue; }
109
+
110
+ if (!existsSync(pkgJsonFile)) continue;
111
+
112
+ try {
113
+ const pkgJson = JSON.parse(await Bun.file(pkgJsonFile).text());
114
+
115
+ const possibleEntries = [
116
+ pkgJson.module,
117
+ pkgJson.browser,
118
+ pkgJson.main,
119
+ 'dist/index.js',
120
+ 'lib/index.js',
121
+ 'index.js',
122
+ ].filter(Boolean);
123
+
124
+ for (const entry of possibleEntries) {
125
+ const fullPath = join(pkgDir, entry);
126
+ if (existsSync(fullPath)) {
127
+ importMap[pkgName] = `/node_modules/${pkgName}/${entry}`;
128
+ logger.debug(`📦 Dev map: ${pkgName} → ${importMap[pkgName]}`);
129
+ break;
130
+ }
131
+ }
132
+ } catch { continue; }
133
+ }
134
+ }
135
+ } catch (error) {
136
+ logger.warn(`Failed to scan node_modules: ${error.message}`);
137
+ }
138
+ }
139
+
140
+ _cachedImportMap = importMap;
141
+ _cachedPkgMtime = currentMtime;
142
+
143
+ logger.success(`✅ Import map ready (${Object.keys(importMap).length} packages)`);
144
+ return importMap;
145
+ }
146
+
147
+ // ─────────────────────────────────────────────────────────────────────────────
148
+ // HTML generator — uses the live importmap so newly installed packages
149
+ // appear in the next page load without a server restart.
150
+ // ─────────────────────────────────────────────────────────────────────────────
151
+ export async function serveHTML(root, hasRouter, config, port) {
152
+ // Don't cache HTML anymore — importmap can change between requests
61
153
  const meta = config.meta || {};
62
-
154
+
63
155
  const srcStylesDir = join(root, 'src', 'styles');
64
156
  let userStylesheets = '';
65
-
157
+
66
158
  if (existsSync(srcStylesDir)) {
67
159
  try {
68
160
  const cssFiles = readdirSync(srcStylesDir).filter(f => f.endsWith('.css'));
69
- userStylesheets = cssFiles.map(f => ` <link rel="stylesheet" href="/styles/${f}">`).join('\n');
161
+ userStylesheets = cssFiles
162
+ .map(f => ` <link rel="stylesheet" href="/styles/${f}">`)
163
+ .join('\n');
70
164
  } catch (error) {
71
165
  logger.warn(`Could not read styles directory: ${error.message}`);
72
166
  }
73
167
  }
74
-
168
+
75
169
  // Auto-detect bertui-animate CSS
76
170
  let bertuiAnimateStylesheet = '';
77
171
  const bertuiAnimatePath = join(root, 'node_modules/bertui-animate/dist/bertui-animate.min.css');
78
172
  if (existsSync(bertuiAnimatePath)) {
79
173
  bertuiAnimateStylesheet = ' <link rel="stylesheet" href="/bertui-animate.css">';
80
174
  }
81
-
82
- // Build import map
83
- const importMap = {
84
- "react": "https://esm.sh/react@18.2.0",
85
- "react-dom": "https://esm.sh/react-dom@18.2.0",
86
- "react-dom/client": "https://esm.sh/react-dom@18.2.0/client"
87
- };
88
-
89
- // Auto-detect bertui-* JavaScript packages
90
- const nodeModulesDir = join(root, 'node_modules');
91
-
92
- if (existsSync(nodeModulesDir)) {
93
- try {
94
- const packages = readdirSync(nodeModulesDir);
95
-
96
- for (const pkg of packages) {
97
- if (!pkg.startsWith('bertui-')) continue;
98
-
99
- const pkgDir = join(nodeModulesDir, pkg);
100
- const pkgJsonPath = join(pkgDir, 'package.json');
101
-
102
- if (!existsSync(pkgJsonPath)) continue;
103
-
104
- try {
105
- const pkgJsonContent = await Bun.file(pkgJsonPath).text();
106
- const pkgJson = JSON.parse(pkgJsonContent);
107
-
108
- let mainFile = null;
109
-
110
- if (pkgJson.exports) {
111
- const rootExport = pkgJson.exports['.'];
112
- if (typeof rootExport === 'string') {
113
- mainFile = rootExport;
114
- } else if (typeof rootExport === 'object') {
115
- mainFile = rootExport.browser || rootExport.default || rootExport.import;
116
- }
117
- }
118
-
119
- if (!mainFile) {
120
- mainFile = pkgJson.main || 'index.js';
121
- }
122
-
123
- const fullPath = join(pkgDir, mainFile);
124
- if (existsSync(fullPath)) {
125
- importMap[pkg] = `/node_modules/${pkg}/${mainFile}`;
126
- logger.debug(`✅ ${pkg} available`);
127
- }
128
-
129
- } catch (error) {
130
- logger.warn(`⚠️ Failed to parse ${pkg}/package.json: ${error.message}`);
131
- }
132
- }
133
- } catch (error) {
134
- logger.warn(`Failed to scan node_modules: ${error.message}`);
135
- }
136
- }
137
-
138
- const html = `<!DOCTYPE html>
175
+
176
+ // Always get the latest importmap (cached until package.json changes)
177
+ const importMap = await buildDevImportMap(root);
178
+
179
+ return `<!DOCTYPE html>
139
180
  <html lang="${meta.lang || 'en'}">
140
181
  <head>
141
182
  <meta charset="UTF-8">
142
183
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
143
184
  <title>${meta.title || 'BertUI App'}</title>
144
-
145
- ${meta.description ? `<meta name="description" content="${meta.description}">` : ''}
146
- ${meta.keywords ? `<meta name="keywords" content="${meta.keywords}">` : ''}
147
- ${meta.author ? `<meta name="author" content="${meta.author}">` : ''}
148
- ${meta.themeColor ? `<meta name="theme-color" content="${meta.themeColor}">` : ''}
149
-
150
- ${meta.ogTitle ? `<meta property="og:title" content="${meta.ogTitle || meta.title}">` : ''}
185
+
186
+ ${meta.description ? `<meta name="description" content="${meta.description}">` : ''}
187
+ ${meta.keywords ? `<meta name="keywords" content="${meta.keywords}">` : ''}
188
+ ${meta.author ? `<meta name="author" content="${meta.author}">` : ''}
189
+ ${meta.themeColor ? `<meta name="theme-color" content="${meta.themeColor}">` : ''}
190
+
191
+ ${meta.ogTitle ? `<meta property="og:title" content="${meta.ogTitle || meta.title}">` : ''}
151
192
  ${meta.ogDescription ? `<meta property="og:description" content="${meta.ogDescription || meta.description}">` : ''}
152
- ${meta.ogImage ? `<meta property="og:image" content="${meta.ogImage}">` : ''}
153
-
193
+ ${meta.ogImage ? `<meta property="og:image" content="${meta.ogImage}">` : ''}
194
+
154
195
  <link rel="icon" type="image/svg+xml" href="/public/favicon.svg">
155
-
196
+
156
197
  ${userStylesheets}
157
198
  ${bertuiAnimateStylesheet}
158
-
199
+
159
200
  <script type="importmap">
160
201
  ${JSON.stringify({ imports: importMap }, null, 2)}
161
202
  </script>
162
-
203
+
163
204
  <style>
164
- * {
165
- margin: 0;
166
- padding: 0;
167
- box-sizing: border-box;
168
- }
169
- body {
170
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
171
- }
205
+ * { margin: 0; padding: 0; box-sizing: border-box; }
206
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
172
207
  </style>
173
208
  </head>
174
209
  <body>
175
210
  <div id="root"></div>
176
-
211
+
177
212
  <script type="module">
178
213
  const ws = new WebSocket('ws://localhost:${port}/__hmr');
179
-
214
+
180
215
  ws.onopen = () => {
181
216
  console.log('%c🔥 BertUI HMR connected', 'color: #10b981; font-weight: bold');
182
217
  };
183
-
218
+
184
219
  ws.onmessage = (event) => {
185
220
  const data = JSON.parse(event.data);
186
-
221
+
187
222
  if (data.type === 'reload') {
188
223
  console.log('%c🔄 Reloading...', 'color: #f59e0b; font-weight: bold');
224
+ if (window.__BERTUI_HIDE_ERROR__) window.__BERTUI_HIDE_ERROR__();
189
225
  window.location.reload();
190
226
  }
191
-
227
+
192
228
  if (data.type === 'recompiling') {
193
229
  console.log('%c⚙️ Recompiling...', 'color: #3b82f6');
194
230
  }
195
-
231
+
196
232
  if (data.type === 'compiled') {
197
233
  console.log('%c✅ Compilation complete', 'color: #10b981');
234
+ if (window.__BERTUI_HIDE_ERROR__) window.__BERTUI_HIDE_ERROR__();
235
+ }
236
+
237
+ // ✅ New: server tells browser the importmap changed → full reload
238
+ if (data.type === 'importmap-updated') {
239
+ console.log('%c📦 New packages detected — reloading...', 'color: #8b5cf6; font-weight: bold');
240
+ window.location.reload();
241
+ }
242
+
243
+ if (data.type === 'compilation-error') {
244
+ if (window.__BERTUI_SHOW_ERROR__) {
245
+ window.__BERTUI_SHOW_ERROR__({
246
+ type: 'Compilation Error',
247
+ message: data.message,
248
+ stack: data.stack,
249
+ file: data.file,
250
+ line: data.line,
251
+ column: data.column,
252
+ });
253
+ }
198
254
  }
199
255
  };
200
-
256
+
201
257
  ws.onerror = (error) => {
202
258
  console.error('%c❌ HMR connection error', 'color: #ef4444', error);
203
259
  };
204
-
260
+
205
261
  ws.onclose = () => {
206
262
  console.log('%c⚠️ HMR disconnected. Refresh to reconnect.', 'color: #f59e0b');
207
263
  };
208
264
  </script>
209
-
265
+
266
+ <script src="/error-overlay.js"></script>
210
267
  <script type="module" src="/compiled/main.js"></script>
211
268
  </body>
212
269
  </html>`;
213
-
214
- // Cache the HTML
215
- globalCache.set(cacheKey, html, { ttl: 1000 });
216
-
217
- return html;
218
270
  }
219
271
 
220
- // File watcher setup (unchanged)
272
+ // ─────────────────────────────────────────────────────────────────────────────
273
+ // File watcher — watches src/ for code changes AND package.json for
274
+ // new installs. When package.json changes it invalidates the importmap
275
+ // cache and sends an `importmap-updated` message so the browser reloads
276
+ // with the new packages — no server restart needed.
277
+ // ─────────────────────────────────────────────────────────────────────────────
221
278
  export function setupFileWatcher(root, compiledDir, clients, onRecompile) {
222
- const srcDir = join(root, 'src');
279
+ const srcDir = join(root, 'src');
280
+ const pkgJson = join(root, 'package.json');
223
281
  const configPath = join(root, 'bertui.config.js');
224
-
282
+
225
283
  if (!existsSync(srcDir)) {
226
284
  logger.warn('src/ directory not found');
227
285
  return () => {};
228
286
  }
229
-
287
+
230
288
  logger.debug(`👀 Watching: ${srcDir}`);
231
-
232
- let isRecompiling = false;
233
- let recompileTimeout = null;
234
- const watchedExtensions = ['.js', '.jsx', '.ts', '.tsx', '.css', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.avif'];
235
-
289
+ logger.debug(`👀 Watching: ${pkgJson} (package installs)`);
290
+
291
+ let isRecompiling = false;
292
+ let recompileTimeout = null;
293
+ const watchedExtensions = [
294
+ '.js', '.jsx', '.ts', '.tsx', '.css',
295
+ '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.avif',
296
+ ];
297
+
236
298
  function notifyClients(message) {
237
299
  for (const client of clients) {
238
300
  try {
239
301
  client.send(JSON.stringify(message));
240
- } catch (e) {
302
+ } catch {
241
303
  clients.delete(client);
242
304
  }
243
305
  }
244
306
  }
245
-
246
- const watcher = watch(srcDir, { recursive: true }, async (eventType, filename) => {
307
+
308
+ // ── Source file watcher ──────────────────────────────────────────────────
309
+ const srcWatcher = watch(srcDir, { recursive: true }, async (eventType, filename) => {
247
310
  if (!filename) return;
248
-
249
311
  const ext = extname(filename);
250
312
  if (!watchedExtensions.includes(ext)) return;
251
-
313
+
252
314
  logger.debug(`📝 File changed: ${filename}`);
253
-
254
315
  clearTimeout(recompileTimeout);
255
-
316
+
256
317
  recompileTimeout = setTimeout(async () => {
257
318
  if (isRecompiling) return;
258
-
259
319
  isRecompiling = true;
260
320
  notifyClients({ type: 'recompiling' });
261
-
321
+
262
322
  try {
263
323
  await compileProject(root);
264
-
265
- if (onRecompile) {
266
- await onRecompile();
267
- }
268
-
324
+ if (onRecompile) await onRecompile();
269
325
  logger.success('✅ Recompiled successfully');
270
326
  notifyClients({ type: 'compiled' });
271
-
272
- setTimeout(() => {
273
- notifyClients({ type: 'reload' });
274
- }, 100);
275
-
327
+ setTimeout(() => notifyClients({ type: 'reload' }), 100);
276
328
  } catch (error) {
277
329
  logger.error(`Recompilation failed: ${error.message}`);
330
+ notifyClients({
331
+ type: 'compilation-error',
332
+ message: error.message,
333
+ stack: error.stack || null,
334
+ file: error.file || null,
335
+ line: error.line || null,
336
+ column: error.column || null,
337
+ });
278
338
  } finally {
279
339
  isRecompiling = false;
280
340
  }
281
341
  }, 150);
282
342
  });
283
-
284
- // Watch config file if it exists
343
+
344
+ // ── package.json watcher detects new npm/bun installs ─────────────────
345
+ let pkgWatcher = null;
346
+ let lastPkgMtime = null;
347
+
348
+ if (existsSync(pkgJson)) {
349
+ try {
350
+ lastPkgMtime = statSync(pkgJson).mtimeMs;
351
+ } catch { /* ignore */ }
352
+
353
+ pkgWatcher = watch(pkgJson, async (eventType) => {
354
+ if (eventType !== 'change') return;
355
+
356
+ // Debounce — installs can trigger multiple change events
357
+ clearTimeout(pkgWatcher._debounce);
358
+ pkgWatcher._debounce = setTimeout(async () => {
359
+ try {
360
+ const newMtime = statSync(pkgJson).mtimeMs;
361
+ if (newMtime === lastPkgMtime) return; // spurious event
362
+ lastPkgMtime = newMtime;
363
+
364
+ logger.info('📦 package.json changed — refreshing import map...');
365
+
366
+ // Bust the importmap cache so next serveHTML call rebuilds it
367
+ _cachedImportMap = null;
368
+ _cachedPkgMtime = null;
369
+
370
+ // Wait briefly for node_modules to finish writing
371
+ await new Promise(r => setTimeout(r, 800));
372
+
373
+ // Rebuild and notify browser
374
+ await buildDevImportMap(root);
375
+ notifyClients({ type: 'importmap-updated' });
376
+
377
+ logger.success('✅ Import map updated — browser reloading');
378
+ } catch (err) {
379
+ logger.warn(`package.json watch error: ${err.message}`);
380
+ }
381
+ }, 500);
382
+ });
383
+ }
384
+
385
+ // ── bertui.config.js watcher ─────────────────────────────────────────────
285
386
  let configWatcher = null;
286
387
  if (existsSync(configPath)) {
287
- configWatcher = watch(configPath, async (eventType) => {
388
+ configWatcher = watch(configPath, (eventType) => {
288
389
  if (eventType === 'change') {
289
390
  logger.debug('📝 Config changed, reloading...');
290
391
  notifyClients({ type: 'reload' });
291
392
  }
292
393
  });
293
394
  }
294
-
395
+
295
396
  // Return cleanup function
296
397
  return () => {
297
- watcher.close();
398
+ srcWatcher.close();
399
+ if (pkgWatcher) pkgWatcher.close();
298
400
  if (configWatcher) configWatcher.close();
299
401
  };
300
402
  }
@@ -0,0 +1,52 @@
1
+ // bertui/src/utils/importhow.js
2
+ import { join, relative, dirname } from 'path';
3
+
4
+ /**
5
+ * @param {Object} importhow - { alias: relPath } from bertui.config.js
6
+ * @param {string} projectRoot - absolute project root
7
+ * @param {string} compiledDir - if set, aliases resolve to compiledDir/<alias>
8
+ * pass .bertui/compiled in dev mode
9
+ * leave null in build mode (uses raw source paths)
10
+ */
11
+ export function buildAliasMap(importhow = {}, projectRoot, compiledDir = null) {
12
+ const map = new Map();
13
+ for (const [alias, relPath] of Object.entries(importhow)) {
14
+ const abs = compiledDir
15
+ ? join(compiledDir, alias) // dev: .bertui/compiled/amani
16
+ : join(projectRoot, relPath); // build: /project/src/components
17
+ map.set(alias, abs);
18
+ }
19
+ return map;
20
+ }
21
+
22
+ /**
23
+ * Rewrite alias import specifiers in compiled code.
24
+ * 'amani/button' → '../components/button.js'
25
+ */
26
+ export function rewriteAliasImports(code, currentFile, aliasMap) {
27
+ if (!aliasMap || aliasMap.size === 0) return code;
28
+
29
+ const currentDir = dirname(currentFile);
30
+ const importRe = /(?:import|export)(?:\s+[\w*{},\s]+\s+from)?\s+['"]([^'"]+)['"]/g;
31
+
32
+ return code.replace(importRe, (match, specifier) => {
33
+ const slashIdx = specifier.indexOf('/');
34
+ const alias = slashIdx === -1 ? specifier : specifier.slice(0, slashIdx);
35
+ const rest = slashIdx === -1 ? '' : specifier.slice(slashIdx);
36
+
37
+ const absBase = aliasMap.get(alias);
38
+ if (!absBase) return match;
39
+
40
+ let rel = relative(currentDir, absBase + rest).replace(/\\/g, '/');
41
+ if (!rel.startsWith('.')) rel = './' + rel;
42
+ if (rest && !/\.\w+$/.test(rest)) rel += '.js';
43
+
44
+ return match.replace(`'${specifier}'`, `'${rel}'`).replace(`"${specifier}"`, `"${rel}"`);
45
+ });
46
+ }
47
+
48
+ export function getAliasDirs(aliasMap) {
49
+ const dirs = new Set();
50
+ for (const absPath of aliasMap.values()) dirs.add(absPath);
51
+ return dirs;
52
+ }