diff --git a/packages/nodejs/src/module-source.ts b/packages/nodejs/src/module-source.ts index 01d68300..221c0453 100644 --- a/packages/nodejs/src/module-source.ts +++ b/packages/nodejs/src/module-source.ts @@ -67,16 +67,8 @@ function expandStarReExports(source: string, hostPath: string): string { if (!targetPath || !existsSync(targetPath)) continue; try { - const targetSource = readFileSync(targetPath, "utf-8"); - const [, targetExports] = parse(targetSource, targetPath); - const names = targetExports - .map((e) => e.n) - .filter( - (n): n is string => - typeof n === "string" && - n !== "default" && - !ownExportNames.has(n), - ); + const names = collectNamedExportsForStarResolution(targetPath) + .filter((n) => n !== "default" && !ownExportNames.has(n)); if (names.length > 0) { // Track these names so subsequent export * don't duplicate @@ -96,6 +88,39 @@ function expandStarReExports(source: string, hostPath: string): string { return result; } +function collectNamedExportsForStarResolution( + filePath: string, + visited = new Set(), +): string[] { + if (visited.has(filePath) || !existsSync(filePath)) { + return []; + } + + visited.add(filePath); + + const source = readFileSync(filePath, "utf-8"); + const starExportRegex = /export\s*\*\s*from\s*['"]([^'"]+)['"]\s*;?/g; + const [, ownExports] = parse(source, filePath); + const names = new Set( + ownExports + .map((e) => e.n) + .filter((n): n is string => typeof n === "string"), + ); + + let match: RegExpExecArray | null; + while ((match = starExportRegex.exec(source)) !== null) { + const specifier = match[1]; + if (!specifier.startsWith(".")) continue; + + const targetPath = pathJoin(pathDirname(filePath), specifier); + for (const name of collectNamedExportsForStarResolution(targetPath, visited)) { + names.add(name); + } + } + + return Array.from(names); +} + function isValidIdentifier(value: string): boolean { return /^[$A-Z_][0-9A-Z_$]*$/i.test(value); } diff --git a/packages/nodejs/test/module-source.test.ts b/packages/nodejs/test/module-source.test.ts index f2f55b12..45b390b0 100644 --- a/packages/nodejs/test/module-source.test.ts +++ b/packages/nodejs/test/module-source.test.ts @@ -1,3 +1,6 @@ +import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { sourceHasModuleSyntax, @@ -51,4 +54,30 @@ describe("module source transforms", () => { await expect(sourceHasModuleSyntax(source, "/pkg/dist/cli.js")).resolves.toBe(true); }); + + it("expands nested star re-exports into named exports", () => { + const tempDir = mkdtempSync(join(tmpdir(), "secure-exec-module-source-")); + const webhooksDir = join(tempDir, "resources", "webhooks"); + const entryPath = join(tempDir, "resources", "webhooks.mjs"); + + mkdirSync(webhooksDir, { recursive: true }); + writeFileSync(entryPath, 'export * from "./webhooks/index.mjs";\n'); + writeFileSync( + join(webhooksDir, "index.mjs"), + 'export * from "./webhooks.mjs";\n', + ); + writeFileSync( + join(webhooksDir, "webhooks.mjs"), + "export class Webhooks {}\n", + ); + + const transformed = transformSourceForImportSync( + readFileSync(entryPath, "utf8"), + entryPath, + ); + + expect(transformed).toContain( + "export { Webhooks } from './webhooks/index.mjs';", + ); + }); });