-
-
Notifications
You must be signed in to change notification settings - Fork 265
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 }) | |
| } | |
| } |