-
-
Notifications
You must be signed in to change notification settings - Fork 260
Description
What's the problem?
When using outExtension: () => ({ js: ".jsx" })
Tsup doesn not generate source maps. The build works but the missing source maps lead to a very bad debugging experience for us. Would be great to have these for jsx outputs as well.
Longer Story
We're using SolidJS and need to preserve the JSX in library code of a big monorepo. The libs are built with tsup while the application is built with vite. The application uses vite-plugin-solid
which expects all library code expose the unbuilt jsx and then builds the JSX in files with .jsx
extensions in one batch. The build works but the missing source maps lead to a very bad debugging experience for us.
Reproduction
// tsup.config.ts
import { defineConfig } from "tsup";
export default defineConfig({
format: "esm",
entry: ["src/index.tsx"],
outDir: "out",
sourcemap: true,
esbuildOptions(es_options) {
es_options.jsx = "preserve";
},
outExtension: () => ({ js: ".jsx" }),
});
// src/index.tsx
export function Component() {
return <div style="color: red; font-weight: bold;">Hello</div>;
}
// package.json
{
"name": "lib",
"type": "module",
"exports": "./out/index.jsx",
"scripts": {
"build": "tsup"
},
"dependencies": {
"solid-js": "~1.9.4"
},
"devDependencies": {
"typescript": "~5.2.2",
"tsup": "~8.3.6"
}
}
Now run tsup
Expected behavior
The out
folder should contain index.jsx
and index.jsx.map
.
Actual behavior
The out
folder only contains index.jsx
and no index.jsx.map
.
Narrowing it down
When commenting out the outExtension
option in the tsup.config.ts
the maps start appearing.
Additional info
It does not seem to be an upstream esbuild problem. I ran esbuild with the same config that tsup calls it with and the source maps were there. They aren't when run through tsup though. I think the issue might lie in the this code that's responsible for writing to disc
Lines 131 to 225 in 0328fd6
const files: Array<ChunkInfo | AssetInfo> = outputFiles | |
.filter((file) => !file.path.endsWith('.map')) | |
.map((file): ChunkInfo | AssetInfo => { | |
if (isJS(file.path) || isCSS(file.path)) { | |
const relativePath = slash(path.relative(process.cwd(), file.path)) | |
const meta = metafile?.outputs[relativePath] | |
return { | |
type: 'chunk', | |
path: file.path, | |
code: file.text, | |
map: outputFiles.find((f) => f.path === `${file.path}.map`)?.text, | |
entryPoint: meta?.entryPoint, | |
exports: meta?.exports, | |
imports: meta?.imports, | |
} | |
} else { | |
return { type: 'asset', path: file.path, contents: file.contents } | |
} | |
}) | |
const writtenFiles: WrittenFile[] = [] | |
await Promise.all( | |
files.map(async (info) => { | |
for (const plugin of this.plugins) { | |
if (info.type === 'chunk' && plugin.renderChunk) { | |
const result = await plugin.renderChunk.call( | |
this.getContext(), | |
info.code, | |
info, | |
) | |
if (result) { | |
info.code = result.code | |
if (result.map) { | |
const originalConsumer = await new SourceMapConsumer( | |
parseSourceMap(info.map), | |
) | |
const newConsumer = await new SourceMapConsumer( | |
parseSourceMap(result.map), | |
) | |
const generator = SourceMapGenerator.fromSourceMap(newConsumer) | |
generator.applySourceMap(originalConsumer, info.path) | |
info.map = generator.toJSON() | |
originalConsumer.destroy() | |
newConsumer.destroy() | |
} | |
} | |
} | |
} | |
const inlineSourceMap = this.context!.options.sourcemap === 'inline' | |
const contents = | |
info.type === 'chunk' | |
? info.code + | |
getSourcemapComment( | |
inlineSourceMap, | |
info.map, | |
info.path, | |
isCSS(info.path), | |
) | |
: info.contents | |
await outputFile(info.path, contents, { | |
mode: info.type === 'chunk' ? info.mode : undefined, | |
}) | |
writtenFiles.push({ | |
get name() { | |
return path.relative(process.cwd(), info.path) | |
}, | |
get size() { | |
return contents.length | |
}, | |
}) | |
if (info.type === 'chunk' && info.map && !inlineSourceMap) { | |
const map = | |
typeof info.map === 'string' ? JSON.parse(info.map) : info.map | |
const outPath = `${info.path}.map` | |
const contents = JSON.stringify(map) | |
await outputFile(outPath, contents) | |
writtenFiles.push({ | |
get name() { | |
return path.relative(process.cwd(), outPath) | |
}, | |
get size() { | |
return contents.length | |
}, | |
}) | |
} | |
}), | |
) | |
for (const plugin of this.plugins) { | |
if (plugin.buildEnd) { | |
await plugin.buildEnd.call(this.getContext(), { writtenFiles }) | |
} | |
} |