Skip to content

Commit 7e91265

Browse files
committed
fix(bundler): enhance chunk extraction logic to locate component exports when proxy files are absent.
1 parent fe529dd commit 7e91265

File tree

1 file changed

+134
-26
lines changed

1 file changed

+134
-26
lines changed

pkg/@eser/bundler/backends/deno-bundler.ts

Lines changed: 134 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@ export class DenoBundlerBackend implements Bundler {
367367

368368
/**
369369
* Find which chunks an entrypoint depends on by analyzing proxy files.
370+
* Also searches all chunks to find where the component is actually exported.
370371
*/
371372
private async findEntrypointChunks(
372373
entrypointPath: string,
@@ -379,28 +380,103 @@ export class DenoBundlerBackend implements Bundler {
379380
const relativePath = entrypointPath.replace(/\.tsx?$/, ".js");
380381
const proxyFilePath = posix.join(scanDir, relativePath);
381382

383+
// Try to read proxy file
384+
let proxyContent: string | null = null;
382385
try {
383-
const content = await runtime.fs.readTextFile(proxyFilePath);
384-
return this.extractChunksFromProxyFile(content, outputs);
386+
proxyContent = await runtime.fs.readTextFile(proxyFilePath);
385387
} catch {
386388
// Try in the nested directory structure
387389
try {
388390
const nestedProxyPath = posix.join(outputDir, "dist", relativePath);
389-
const content = await runtime.fs.readTextFile(nestedProxyPath);
390-
return this.extractChunksFromProxyFile(content, outputs);
391+
proxyContent = await runtime.fs.readTextFile(nestedProxyPath);
391392
} catch {
392-
// No proxy file found - this might be the main entry
393-
return [];
393+
// No proxy file found
394394
}
395395
}
396+
397+
// If we have a proxy file, use it
398+
if (proxyContent !== null) {
399+
return this.extractChunksFromProxyFile(proxyContent, outputs);
400+
}
401+
402+
// No proxy file - search all chunks for the component export
403+
// This handles cases where Deno.bundle doesn't create proxy files
404+
return this.findChunksForComponentName(entrypointPath, outputs);
405+
}
406+
407+
/**
408+
* Find chunks containing a component by searching all chunk exports.
409+
* Used when no proxy file exists for an entrypoint.
410+
*/
411+
private findChunksForComponentName(
412+
entrypointPath: string,
413+
outputs: Map<string, BundleOutput>,
414+
): string[] {
415+
const chunks: string[] = [];
416+
417+
// Extract expected export name from file path
418+
// e.g., "src/app/icon.tsx" -> "Icon" (PascalCase of basename)
419+
const basename = posix.basename(entrypointPath).replace(/\.[^.]+$/, "");
420+
const expectedExport = basename.charAt(0).toUpperCase() +
421+
basename.slice(1).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
422+
423+
// Search all chunks for the exported component
424+
for (const [chunkFile, chunkOutput] of outputs) {
425+
// Only check chunk files
426+
if (!chunkFile.startsWith("chunk-") || !chunkFile.endsWith(".js")) {
427+
continue;
428+
}
429+
430+
const chunkContent = new TextDecoder().decode(chunkOutput.code);
431+
432+
// Check if this chunk exports the expected symbol
433+
const exportPatterns = [
434+
// export { Symbol } or export { Symbol, ... } or export { X as Symbol }
435+
new RegExp(`export\\s*\\{[^}]*\\b${expectedExport}\\b[^}]*\\}`),
436+
// export function Symbol or export const Symbol
437+
new RegExp(
438+
`export\\s+(?:function|const|let|var|class)\\s+${expectedExport}\\b`,
439+
),
440+
// minified pattern: Symbol2 as Symbol
441+
new RegExp(`\\b\\w+\\s+as\\s+${expectedExport}\\b`),
442+
];
443+
444+
const exportsSymbol = exportPatterns.some((pattern) =>
445+
pattern.test(chunkContent)
446+
);
447+
448+
if (exportsSymbol) {
449+
// Found the main chunk - add it to the front
450+
chunks.unshift(chunkFile);
451+
452+
// Also find dependency chunks by looking at what this chunk imports
453+
const importPattern =
454+
/from\s*["']\.?\/?([^"']*chunk-[A-Z0-9]+\.js)["']/gi;
455+
let match: RegExpExecArray | null;
456+
while ((match = importPattern.exec(chunkContent)) !== null) {
457+
const depChunk = posix.basename(match[1] ?? "");
458+
if (depChunk && !chunks.includes(depChunk)) {
459+
chunks.push(depChunk);
460+
}
461+
}
462+
break;
463+
}
464+
}
465+
466+
return chunks;
396467
}
397468

398469
/**
399470
* Extract chunk dependencies from a proxy file's import statements.
471+
* Returns chunks with the main chunk (containing the exported symbol) first.
472+
*
473+
* IMPORTANT: Due to code splitting, the proxy file may not import from the chunk
474+
* that actually contains the exported component. We must search ALL chunks to find
475+
* the one that exports the symbol.
400476
*/
401477
private extractChunksFromProxyFile(
402478
content: string,
403-
_outputs: Map<string, BundleOutput>,
479+
outputs: Map<string, BundleOutput>,
404480
): string[] {
405481
const chunks: string[] = [];
406482

@@ -411,7 +487,7 @@ export class DenoBundlerBackend implements Bundler {
411487

412488
let match: RegExpExecArray | null;
413489

414-
// Find all chunk imports
490+
// Find all chunk imports from proxy file
415491
while ((match = chunkImportPattern.exec(content)) !== null) {
416492
const importPath = match[1];
417493
if (importPath !== undefined) {
@@ -433,32 +509,64 @@ export class DenoBundlerBackend implements Bundler {
433509
}
434510
}
435511

436-
// Determine main chunk (the one that exports the component)
512+
// Determine main chunk by finding which chunk actually exports the symbol
513+
// IMPORTANT: Search ALL chunks in the bundle, not just the ones imported by proxy
437514
const exportMatch = content.match(/export\s*\{([^}]+)\}/);
438-
if (exportMatch !== null && chunks.length > 0) {
515+
if (exportMatch !== null) {
439516
const exportStatement = exportMatch[1];
440-
const symbolMatch = exportStatement?.match(/(\w+)\s+as\s+/);
441-
const exportedSymbol = symbolMatch !== null && symbolMatch !== undefined
442-
? symbolMatch[1]
443-
: null;
517+
// Match either "Symbol as ExportName" or just "Symbol"
518+
const symbolMatch = exportStatement?.match(/(\w+)(?:\s+as\s+\w+)?/);
519+
const exportedSymbol = symbolMatch?.[1] ?? null;
444520

445521
if (exportedSymbol !== null) {
446-
// Find which chunk exports this symbol
447-
for (const chunkFile of chunks) {
448-
const chunkName = chunkFile.replace(/\.js$/, "");
449-
const importPattern = new RegExp(
450-
`import\\s*\\{[^}]*\\b${exportedSymbol}\\b[^}]*\\}\\s*from\\s*["'][^"']*${chunkName}`,
522+
// Search ALL chunks in the bundle to find which one exports the symbol
523+
// This handles code splitting where the actual component ends up in a shared chunk
524+
let mainChunkFile: string | null = null;
525+
526+
for (const [chunkFile, chunkOutput] of outputs) {
527+
// Only check chunk files (not main entry or other files)
528+
if (!chunkFile.startsWith("chunk-") || !chunkFile.endsWith(".js")) {
529+
continue;
530+
}
531+
532+
const chunkContent = new TextDecoder().decode(chunkOutput.code);
533+
534+
// Check if this chunk exports the symbol directly
535+
const exportPatterns = [
536+
// export { Symbol } or export { Symbol, ... } or export { X as Symbol }
537+
new RegExp(`export\\s*\\{[^}]*\\b${exportedSymbol}\\b[^}]*\\}`),
538+
// export function Symbol or export const Symbol
539+
new RegExp(
540+
`export\\s+(?:function|const|let|var|class)\\s+${exportedSymbol}\\b`,
541+
),
542+
// minified pattern: Symbol2 as Symbol (common in bundled code)
543+
new RegExp(`\\b\\w+\\s+as\\s+${exportedSymbol}\\b`),
544+
];
545+
546+
const exportsSymbol = exportPatterns.some((pattern) =>
547+
pattern.test(chunkContent)
451548
);
452-
if (importPattern.test(content)) {
453-
// Move main chunk to front
454-
const mainIndex = chunks.indexOf(chunkFile);
455-
if (mainIndex > 0) {
456-
chunks.splice(mainIndex, 1);
457-
chunks.unshift(chunkFile);
458-
}
549+
550+
if (exportsSymbol) {
551+
mainChunkFile = chunkFile;
459552
break;
460553
}
461554
}
555+
556+
if (mainChunkFile !== null) {
557+
// Ensure main chunk is in the list and at the front
558+
const mainIndex = chunks.indexOf(mainChunkFile);
559+
if (mainIndex > 0) {
560+
// Already in list but not first - move to front
561+
chunks.splice(mainIndex, 1);
562+
chunks.unshift(mainChunkFile);
563+
} else if (mainIndex === -1) {
564+
// Not in list from proxy imports - add to front
565+
// This happens when code splitting puts the component in a shared chunk
566+
chunks.unshift(mainChunkFile);
567+
}
568+
// mainIndex === 0 means already first, no change needed
569+
}
462570
}
463571
}
464572

0 commit comments

Comments
 (0)