thunderous-server 0.0.0 → 0.0.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.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Thunderous Server
2
2
 
3
3
  > [!CAUTION]
4
- > This project is experimental. It is not ready for production use at this time.
4
+ > This project is experimental. It may not be suitable for production use at this time, as it is subject to bugs and breaking changes.
5
5
 
6
6
  **Thunderous Server is a web framework and static generator designed to support and supplement _plain old HTML_.**
7
7
 
@@ -119,6 +119,7 @@ Exported values are available in <script expr> tags.
119
119
 
120
120
  ## TODO
121
121
 
122
+ - [ ] Add support for named slots in layouts
122
123
  - [ ] Create (or find existing) extension that supports:
123
124
  - [ ] TypeScript inside `<script>` tags
124
125
  - [ ] IDE navigation in `<script server>` and `<script expr>` tags (go to definition, find references, etc.)
@@ -127,3 +128,8 @@ Exported values are available in <script expr> tags.
127
128
  - [ ] Typechecking to ensure the default export in `<script server>` is `Record<PropertyKey, unknown>`
128
129
  - [ ] OPTIONAL: Lint and autofix for trailing `;` in `<script expr>` tags
129
130
  _e.g., `('hello';)` is an invalid expression, so `<script expr>'hello';</script>` is technically incorrect. That said, Thunderous Server does handle this currently by stripping it from the content before it's evaluated._
131
+
132
+ ### Known Issues
133
+
134
+ - [ ] The dev server is not reliably hot-reloading changes
135
+ - [ ] The CSR views are not behaving as expected -- unclear if this is a bug in the server or the CSR package.
package/dist/index.cjs CHANGED
@@ -31,7 +31,8 @@ var metaState = {
31
31
  config: {
32
32
  name: "",
33
33
  baseDir: "",
34
- outDir: ""
34
+ outDir: "",
35
+ configDir: null
35
36
  },
36
37
  pathname: "/",
37
38
  breadcrumbs: [],
package/dist/index.d.cts CHANGED
@@ -14,6 +14,10 @@ type ThunderousConfig = {
14
14
  * The output directory for builds.
15
15
  */
16
16
  outDir: string;
17
+ /**
18
+ * Non-configurable. The directory where the `thunderous.config.ts` file is located.
19
+ */
20
+ readonly configDir: string | null;
17
21
  };
18
22
 
19
23
  /**
package/dist/index.d.ts CHANGED
@@ -14,6 +14,10 @@ type ThunderousConfig = {
14
14
  * The output directory for builds.
15
15
  */
16
16
  outDir: string;
17
+ /**
18
+ * Non-configurable. The directory where the `thunderous.config.ts` file is located.
19
+ */
20
+ readonly configDir: string | null;
17
21
  };
18
22
 
19
23
  /**
package/dist/index.js CHANGED
@@ -3,7 +3,8 @@ var metaState = {
3
3
  config: {
4
4
  name: "",
5
5
  baseDir: "",
6
- outDir: ""
6
+ outDir: "",
7
+ configDir: null
7
8
  },
8
9
  pathname: "/",
9
10
  breadcrumbs: [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thunderous-server",
3
- "version": "0.0.0",
3
+ "version": "0.0.2",
4
4
  "description": "A simple server to enhance Thunderous components with SSR.",
5
5
  "license": "MIT",
6
6
  "author": "Jonathan DeWitt <jon.dewitt@thunder.solutions>",
@@ -30,9 +30,7 @@
30
30
  "@types/node": "^24.9.1",
31
31
  "@types/resolve": "^1.20.6",
32
32
  "@typescript-eslint/eslint-plugin": "^8.46.2",
33
- "connect-livereload": "^0.6.1",
34
33
  "eslint": "^9.38.0",
35
- "livereload": "^0.10.3",
36
34
  "prettier": "^3.6.2",
37
35
  "serve": "^14.2.5",
38
36
  "tsx": "^4.20.6",
@@ -41,13 +39,20 @@
41
39
  "undici-types": "^7.16.0"
42
40
  },
43
41
  "dependencies": {
42
+ "chalk": "^5.6.2",
43
+ "connect-livereload": "^0.6.1",
44
+ "emoji-space-shim": "^0.1.7",
44
45
  "es-module-lexer": "^1.7.0",
45
46
  "express": "^5.1.0",
47
+ "livereload": "^0.10.3",
48
+ "node-emoji": "^2.2.0",
49
+ "nodemon": "^3.1.14",
46
50
  "resolve": "^1.22.11",
47
- "resolve.exports": "^2.0.3"
51
+ "resolve.exports": "^2.0.3",
52
+ "string-width": "^8.2.0"
48
53
  },
49
54
  "peerDependencies": {
50
- "thunderous": ">=2.4.1"
55
+ "thunderous": ">=2.4.2"
51
56
  },
52
57
  "scripts": {
53
58
  "build": "tsup --no-clean",
package/src/cli.ts CHANGED
@@ -1,13 +1,43 @@
1
1
  import { build } from './build';
2
- import { dev } from './dev';
2
+ import nodemon from 'nodemon';
3
+ import { readFileSync } from 'fs';
4
+ import { config } from './config';
5
+ import { relative, resolve } from 'path';
6
+ import chalk from 'chalk';
3
7
 
4
8
  const args = process.argv.slice(2);
5
9
 
6
10
  if (args[0] === 'dev') {
7
11
  try {
8
- dev();
12
+ const ignoreFile = readFileSync('.gitignore', 'utf-8');
13
+ const ignores = ignoreFile.split('\n').filter((line) => line.trim() !== '' && !line.startsWith('#'));
14
+
15
+ // Set up nodemon for auto-restart on server changes
16
+ if (process.env.NODE_ENV !== 'production') {
17
+ nodemon({
18
+ script: resolve(`${import.meta.dirname}/dev.ts`),
19
+ ignore: ignores,
20
+ watch: [config.baseDir],
21
+ exec: 'tsx',
22
+ });
23
+
24
+ nodemon
25
+ .on('start', () => {
26
+ // console.log('App has started');
27
+ })
28
+ .on('quit', () => {
29
+ console.log(chalk.green('\nServer shut down successfully.\n'));
30
+ process.exit();
31
+ })
32
+ .on('restart', (files) => {
33
+ const filesList = (files ?? ['(none)']).map(
34
+ (file) => ` • ${chalk.cyan.underline(relative(config.configDir ?? process.cwd(), file))}\n`,
35
+ );
36
+ console.log('\nUpdates detected:\n', filesList.join(''));
37
+ });
38
+ }
9
39
  } catch (error) {
10
- console.error('Failed to start server:', error);
40
+ console.error('\x1b[31mFailed to start server:\x1b[0m', error);
11
41
  process.exit(1);
12
42
  }
13
43
  } else if (args[0] === 'build') {
package/src/config.ts CHANGED
@@ -19,12 +19,17 @@ export type ThunderousConfig = {
19
19
  * The output directory for builds.
20
20
  */
21
21
  outDir: string;
22
+ /**
23
+ * Non-configurable. The directory where the `thunderous.config.ts` file is located.
24
+ */
25
+ readonly configDir: string | null;
22
26
  };
23
27
 
24
28
  const DEFAULT_CONFIG = Object.freeze({
25
29
  name: 'Thunderous Project',
26
30
  baseDir: 'src',
27
31
  outDir: 'dist',
32
+ configDir: null,
28
33
  });
29
34
 
30
35
  /** Find and import the `thunderous.config.ts` file. */
@@ -41,12 +46,13 @@ const resolveConfig = (): ThunderousConfig => {
41
46
  configDir = currentDir;
42
47
  }
43
48
  if (rootDir !== undefined && configDir !== undefined) {
44
- const configPath = join(configDir!, 'thunderous.config.ts');
49
+ const configPath = join(configDir, 'thunderous.config.ts');
45
50
  const configOverrides: Partial<ThunderousConfig> = require(configPath);
46
51
  return {
47
52
  name: configOverrides.name ?? DEFAULT_CONFIG.name,
48
53
  baseDir: configOverrides.baseDir?.replace(/^\/*/, '') ?? DEFAULT_CONFIG.baseDir,
49
54
  outDir: configOverrides.outDir ?? DEFAULT_CONFIG.outDir,
55
+ configDir,
50
56
  };
51
57
  }
52
58
  const parentDir = resolve(currentDir, '..');
package/src/dev.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import express from 'express';
2
- import { readdirSync, statSync, mkdtempSync } from 'fs';
2
+ import { existsSync, readdirSync, readFileSync, statSync, mkdtempSync } from 'fs';
3
3
  import { join, relative, resolve } from 'path';
4
4
  import { tmpdir } from 'os';
5
+ import { ModuleKind, ScriptTarget, transpileModule, ImportsNotUsedAsValues } from 'typescript';
5
6
  import { bootstrapThunderous, generateStaticTemplate, generateImportMap, injectImportMap } from './generate';
6
7
  import { config } from './config';
7
8
  import livereload from 'livereload';
@@ -32,7 +33,7 @@ const bootstrapRoutes = (dir: string, app: express.Express, vendorDir: string) =
32
33
 
33
34
  // Generate import map if there are client entry files
34
35
  if (result.clientEntryFiles.length > 0) {
35
- const importMapJson = generateImportMap(result.clientEntryFiles);
36
+ const importMapJson = generateImportMap(result.clientEntryFiles, vendorDir);
36
37
  markup = injectImportMap(markup, importMapJson);
37
38
  }
38
39
 
@@ -51,7 +52,7 @@ const bootstrapRoutes = (dir: string, app: express.Express, vendorDir: string) =
51
52
  export const dev = () => {
52
53
  console.log('\n\x1b[36m\x1b[1m⚡⚡ Starting development server... ⚡⚡\x1b[0m\x1b[0m\n');
53
54
 
54
- const PORT = process.env['PORT'] ?? 3000;
55
+ const PORT = process.env.PORT ?? 3000;
55
56
 
56
57
  // Set up simple express server
57
58
  const app = express();
@@ -81,6 +82,29 @@ export const dev = () => {
81
82
  bootstrapThunderous();
82
83
  bootstrapRoutes(`./${config.baseDir}`, app, vendorDir);
83
84
 
85
+ // Serve .js requests by transpiling the corresponding .ts source on-the-fly
86
+ app.use((req, res, next) => {
87
+ if (!req.path.endsWith('.js')) return next();
88
+ const tsPath = join(config.baseDir, req.path.replace(/\.js$/, '.ts'));
89
+ const tsxPath = join(config.baseDir, req.path.replace(/\.js$/, '.tsx'));
90
+ const srcPath = existsSync(tsPath) ? tsPath : existsSync(tsxPath) ? tsxPath : null;
91
+ if (srcPath === null) return next();
92
+ const source = readFileSync(srcPath, 'utf-8');
93
+ const { outputText } = transpileModule(source, {
94
+ compilerOptions: {
95
+ module: ModuleKind.ESNext,
96
+ target: ScriptTarget.ES2020,
97
+ sourceMap: false,
98
+ importsNotUsedAsValues: ImportsNotUsedAsValues.Remove,
99
+ verbatimModuleSyntax: true,
100
+ isolatedModules: true,
101
+ },
102
+ fileName: srcPath,
103
+ reportDiagnostics: false,
104
+ });
105
+ res.type('application/javascript').send(outputText);
106
+ });
107
+
84
108
  // Serve static assets from the base directory
85
109
  app.use(express.static(config.baseDir));
86
110
 
@@ -111,3 +135,6 @@ export const dev = () => {
111
135
  });
112
136
  });
113
137
  };
138
+
139
+ // start directly, since this file is targeted directly by nodemon
140
+ dev();
package/src/generate.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
2
2
  import { join, relative, resolve } from 'path';
3
3
  import { createRequire } from 'node:module';
4
4
  import { html } from 'thunderous';
@@ -17,6 +17,7 @@ const outRequire = createRequire(resolve(config.baseDir));
17
17
  // track state outside the function so that we only have to set one `onServerDefine` handler
18
18
  const renderState = {
19
19
  markup: '',
20
+ insertedTags: new Set<string>(),
20
21
  };
21
22
 
22
23
  // ── Shared TypeScript compiler options (created once, reused everywhere) ──
@@ -43,6 +44,8 @@ export const bootstrapThunderous = () => {
43
44
  const { insertTemplates, onServerDefine } = Thunderous;
44
45
  // Update the markup each time a thunderous element is defined on the server
45
46
  onServerDefine((tagName, innerHTML) => {
47
+ if (renderState.insertedTags.has(tagName)) return;
48
+ renderState.insertedTags.add(tagName);
46
49
  renderState.markup = insertTemplates(
47
50
  tagName,
48
51
  innerHTML.replace(/\s+/gm, ' ').replace(/ >/g, '>'),
@@ -105,6 +108,13 @@ const extractTags = (markup: string) => {
105
108
  continue;
106
109
  }
107
110
 
111
+ // Skip HTML comments <!-- ... -->
112
+ if (markup.startsWith('<!--', i)) {
113
+ const commentEnd = markup.indexOf('-->', i + 4);
114
+ i = commentEnd === -1 ? markup.length : commentEnd + 3;
115
+ continue;
116
+ }
117
+
108
118
  // <?layout href="…">
109
119
  if (markup.startsWith('<?layout', i)) {
110
120
  const close = markup.indexOf('>', i);
@@ -203,6 +213,7 @@ const extractTags = (markup: string) => {
203
213
  */
204
214
  export const generateStaticTemplate = (filePath: string) => {
205
215
  renderState.markup = readFileSync(filePath, 'utf-8');
216
+ renderState.insertedTags.clear();
206
217
 
207
218
  const name = basename(filePath, extname(filePath));
208
219
 
@@ -244,7 +255,8 @@ export const generateStaticTemplate = (filePath: string) => {
244
255
  // ── Extract and apply layouts ──
245
256
  const { layouts } = extractTags(renderState.markup);
246
257
  for (let i = layouts.length - 1; i >= 0; i--) {
247
- const l = layouts[i]!;
258
+ const l = layouts[i];
259
+ if (!l) continue;
248
260
  renderState.markup = renderState.markup.slice(0, l.start) + renderState.markup.slice(l.end);
249
261
  }
250
262
  renderState.markup = renderState.markup.trim();
@@ -277,6 +289,8 @@ export const generateStaticTemplate = (filePath: string) => {
277
289
  const scriptKey = (s: ParsedScript) => `${s.kind}|${s.href ?? ''}|${s.content}`;
278
290
  const replacementMap = new Map<string, string>();
279
291
 
292
+ mkdirSync(resolvedOutDir, { recursive: true });
293
+
280
294
  let scriptIndex = 0;
281
295
  for (const script of scripts) {
282
296
  const key = scriptKey(script);
@@ -309,14 +323,14 @@ export const generateStaticTemplate = (filePath: string) => {
309
323
  delete outRequire.cache[outRequire.resolve(hrefAbsPath)];
310
324
  module = outRequire(hrefAbsPath);
311
325
  } else {
312
- // inline: write temp .ts so outRequire can import it
313
- const tsScriptFile = join(resolvedOutDir, `${name}-${scriptIndex}.tmp.ts`);
326
+ // inline: write temp .ts into baseDir so relative imports resolve correctly
327
+ const tsScriptFile = join(resolvedBaseDir, `${name}-${scriptIndex}.tmp.ts`);
314
328
  writeFileSync(tsScriptFile, `// @ts-nocheck\n${content}`, 'utf-8');
315
329
  tempFilesToCleanup.push(tsScriptFile);
316
330
  delete outRequire.cache[outRequire.resolve(tsScriptFile)];
317
331
  module = outRequire(tsScriptFile);
318
332
  }
319
- const values = module['default'] ?? {};
333
+ const values = module.default ?? {};
320
334
  for (const k in values) {
321
335
  const val = (values as Record<string, unknown>)[k];
322
336
  safeValues[k] = typeof val === 'string' ? escapeHtml(val) : val;
@@ -340,11 +354,14 @@ export const generateStaticTemplate = (filePath: string) => {
340
354
  const js = transpileTs(`// @ts-nocheck\n${content}`, `${name}-${scriptIndex}.ts`);
341
355
  const fixedJs = js
342
356
  .replace(/(import\s+.+?\s+from\s+['"](?:\.\.?\/|\/)[^'"]+?)\.tsx?(['"])/gm, '$1.js$2')
343
- .replace(/(import\s+.+?\s+from\s+['"](?:\.\.?\/|\/)[^'"]+?)(?<!\.m?js)(['"])/gm, '$1.js$2');
357
+ .replace(/(import\s+.+?\s+from\s+['"](?:\.\.?\/|\/)[^'"]+?)(?<!\.m?js)(['"])/gm, '$1.js$2')
358
+ // Convert relative ./ imports to absolute / so they resolve correctly
359
+ // regardless of the page's URL depth (e.g. /about/contact)
360
+ .replace(/(import\s+.+?\s+from\s+['"])\.\//gm, '$1/');
344
361
  replacementMap.set(key, `<script type="module">\n${fixedJs}\n</script>`);
345
362
 
346
- // Write .js to outDir for vendorization
347
- const jsOutPath = join(resolvedOutDir, `${name}-${scriptIndex}.tmp.js`);
363
+ // Write .js to baseDir for vendorization (so relative imports resolve from source tree)
364
+ const jsOutPath = join(resolvedBaseDir, `${name}-${scriptIndex}.tmp.js`);
348
365
  writeFileSync(jsOutPath, js, 'utf-8');
349
366
  tempFilesToCleanup.push(jsOutPath);
350
367
  clientEntryFiles.push(resolve(jsOutPath));
@@ -356,7 +373,8 @@ export const generateStaticTemplate = (filePath: string) => {
356
373
  // ── Re-parse and apply all replacements in one reverse pass ──
357
374
  const { scripts: freshScripts } = extractTags(renderState.markup);
358
375
  for (let i = freshScripts.length - 1; i >= 0; i--) {
359
- const script = freshScripts[i]!;
376
+ const script = freshScripts[i];
377
+ if (!script) continue;
360
378
  const key = scriptKey(script);
361
379
  const replacement = replacementMap.get(key);
362
380
  if (replacement === undefined) continue;
@@ -403,16 +421,17 @@ export const generateStaticTemplate = (filePath: string) => {
403
421
  * Generate import maps from entry files using vendorization.
404
422
  * Returns the import map JSON string that can be injected into HTML.
405
423
  */
406
- export const generateImportMap = (entryFiles: string[]): string => {
424
+ export const generateImportMap = (entryFiles: string[], outputDir?: string): string => {
425
+ const outDir = outputDir ?? config.outDir;
407
426
  if (entryFiles.length === 0) {
408
427
  return JSON.stringify({ imports: {} }, null, 2);
409
428
  }
410
429
 
411
430
  console.log(`\x1b[90mVendorizing dependencies from ${entryFiles.length} entry file(s)...\x1b[0m`);
412
- processNodeModules(entryFiles, config.outDir);
431
+ processNodeModules(entryFiles, outDir);
413
432
  console.log(`\x1b[32m✓ Import map generated\x1b[0m`);
414
433
 
415
- const importMapPath = join(config.outDir, 'importmap.json');
434
+ const importMapPath = join(outDir, 'importmap.json');
416
435
  if (existsSync(importMapPath)) {
417
436
  return readFileSync(importMapPath, 'utf-8');
418
437
  }
@@ -423,27 +442,41 @@ export const generateImportMap = (entryFiles: string[]): string => {
423
442
  /**
424
443
  * Inject an import map into HTML content.
425
444
  * If an import map script already exists, it will be replaced.
426
- * Otherwise, it will be injected before the first script tag or at the end.
445
+ * Otherwise, it will be injected into the <head> so it's in a consistent
446
+ * position across all pages (important for CSR partial-update validation).
427
447
  */
428
448
  export const injectImportMap = (html: string, importMapJson: string): string => {
429
- const importMapTag = `<script type="importmap">\n${importMapJson}\n</script>`;
430
-
431
- // Check if there's already an import map and replace it
432
- const existingImportMapRegex = /<script\s+type="importmap"[^>]*>[\s\S]*?<\/script>/i;
433
- if (existingImportMapRegex.test(html)) {
434
- return html.replace(existingImportMapRegex, importMapTag);
435
- }
449
+ const scriptId = `IMPORTMAP_${crypto.randomUUID()}`;
450
+ const importMapTag = `<script type="importmap" id="${scriptId}">\n${importMapJson}\n</script>`;
436
451
 
437
- // Otherwise inject before first script tag
438
- const firstScriptMatch = /<script/i.exec(html);
439
- if (firstScriptMatch?.index !== undefined) {
440
- return html.slice(0, firstScriptMatch.index) + importMapTag + '\n' + html.slice(firstScriptMatch.index);
452
+ // Check if we've already injected an import map (by our ID pattern) and replace it
453
+ const ownImportMapRegex = /<script\s+type="importmap"\s+id="IMPORTMAP_[^"]*"[^>]*>[\s\S]*?<\/script>/i;
454
+ if (ownImportMapRegex.test(html)) {
455
+ return html.replace(ownImportMapRegex, importMapTag);
441
456
  }
442
457
 
443
- // No script tag found, inject before </head> if possible
458
+ // Inject before </head> so the import map is always in a consistent position
444
459
  const headCloseMatch = /<\/head>/i.exec(html);
445
460
  if (headCloseMatch?.index !== undefined) {
446
- return html.slice(0, headCloseMatch.index) + importMapTag + '\n' + html.slice(headCloseMatch.index);
461
+ return html.slice(0, headCloseMatch.index) + '\t' + importMapTag + '\n' + html.slice(headCloseMatch.index);
462
+ }
463
+
464
+ // Fallback: inject before first script tag (skip HTML comments)
465
+ let searchPos = 0;
466
+ while (searchPos < html.length) {
467
+ // Skip HTML comments
468
+ const commentStart = html.indexOf('<!--', searchPos);
469
+ const scriptMatch = /<script/i.exec(html.slice(searchPos));
470
+ if (scriptMatch === null) break;
471
+ const scriptPos = searchPos + scriptMatch.index;
472
+ // If a comment starts before this <script, skip past the comment
473
+ if (commentStart !== -1 && commentStart < scriptPos) {
474
+ const commentEnd = html.indexOf('-->', commentStart + 4);
475
+ searchPos = commentEnd === -1 ? html.length : commentEnd + 3;
476
+ continue;
477
+ }
478
+ // Found a real <script outside of a comment
479
+ return html.slice(0, scriptPos) + importMapTag + '\n' + html.slice(scriptPos);
447
480
  }
448
481
 
449
482
  // Last resort: add at the end
package/src/meta.ts CHANGED
@@ -56,6 +56,7 @@ const metaState: Meta = {
56
56
  name: '',
57
57
  baseDir: '',
58
58
  outDir: '',
59
+ configDir: null,
59
60
  },
60
61
  pathname: '/',
61
62
  breadcrumbs: [],
package/src/vendorize.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { accessSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
1
+ import { accessSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import path, { dirname, relative, join } from 'node:path';
3
3
  import resolve from 'resolve';
4
4
  import { resolve as resolveExports } from 'resolve.exports';
@@ -31,7 +31,21 @@ export function processNodeModules(entryFiles: string[], outputDir?: string) {
31
31
  if (seen.has(file)) continue;
32
32
  seen.add(file);
33
33
 
34
- const code = readFileSync(file, 'utf8');
34
+ // In dev mode, .js may not exist yet; fall back to .ts source and transpile
35
+ let code: string;
36
+ if (existsSync(file)) {
37
+ code = readFileSync(file, 'utf8');
38
+ } else {
39
+ const tsFile = file.replace(/\.js$/, '.ts');
40
+ const tsxFile = file.replace(/\.js$/, '.tsx');
41
+ if (existsSync(tsFile)) {
42
+ code = transpileTsFast(readFileSync(tsFile, 'utf8'), tsFile);
43
+ } else if (existsSync(tsxFile)) {
44
+ code = transpileTsFast(readFileSync(tsxFile, 'utf8'), tsxFile);
45
+ } else {
46
+ throw new Error(`Cannot find module: ${file} (also tried .ts/.tsx)`);
47
+ }
48
+ }
35
49
  const [imports] = parse(code);
36
50
 
37
51
  for (const im of imports) {
@@ -49,12 +63,12 @@ export function processNodeModules(entryFiles: string[], outputDir?: string) {
49
63
  // Prefer declared export for browser/import
50
64
  const sub = resolveExports(pkg, '.', { conditions: CONDITIONS });
51
65
  if (sub) {
52
- pkg['main'] = sub;
66
+ pkg.main = sub;
53
67
  return pkg;
54
68
  }
55
69
  // If no exports field, prefer "module" over "main" for ESM
56
- if (pkg['module']) {
57
- pkg['main'] = pkg['module'];
70
+ if (pkg.module) {
71
+ pkg.main = pkg.module;
58
72
  }
59
73
  // Otherwise fall back to pkg.main (default behavior)
60
74
  return pkg;
@@ -84,7 +98,9 @@ export function processNodeModules(entryFiles: string[], outputDir?: string) {
84
98
 
85
99
  function withJsOrTs(p: string) {
86
100
  const cand = [p, p + '.mjs', p + '.js', p + '.ts', p + '.tsx'];
87
- for (const c of cand) return c; // optimistic; upstream readFile will fail fast if wrong
101
+ for (const c of cand) {
102
+ if (existsSync(c)) return c;
103
+ }
88
104
  return p;
89
105
  }
90
106
 
@@ -92,7 +108,7 @@ function vendorizeFile(absPath: string, vendorRoot: string) {
92
108
  // Find package root & metadata
93
109
  const pkgRoot = findPkgRoot(absPath);
94
110
  const pkg = JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf8')) as Record<string, string>;
95
- const versionTag = `${pkg['name']!.replace('/', '__')}@${pkg['version']!}`;
111
+ const versionTag = `${pkg.name?.replace('/', '__') ?? 'unknown'}@${pkg.version ?? '0.0.0'}`;
96
112
  const relFromPkg = relative(pkgRoot, absPath);
97
113
  const outDir = join(vendorRoot, versionTag);
98
114
  const outPath = join(outDir, relFromPkg.replace(/\.(ts|tsx)$/, '.js'));