diff --git a/docs/README.md b/docs/README.md index cf5c33d9..5526de76 100644 --- a/docs/README.md +++ b/docs/README.md @@ -163,6 +163,10 @@ If you want to inline sourcemap, you can try: tsup index.ts --sourcemap inline ``` +> Warn: Note that inline sourcemap is solely used for development, e.g. when developing a browser extension and the access to `.map` file is not allowed, and it's not recommended for production. + +> Warn: Source map is not supported in `--dts` build. + ### Bundle formats Supported format: `esm`, `cjs`, (default) and `iife`. diff --git a/src/cli-main.ts b/src/cli-main.ts index edf3478f..42e1a5fd 100644 --- a/src/cli-main.ts +++ b/src/cli-main.ts @@ -39,8 +39,8 @@ export async function main(options: Options = {}) { .option('--dts-resolve', 'Resolve externals types used for d.ts files') .option('--dts-only', 'Emit declaration files only') .option( - '--sourcemap [option]', - 'Generate sourcemap, "external", "inline", "both"' + '--sourcemap [inline]', + 'Generate external sourcemap, or inline source: --sourcemap inline' ) .option( '--watch [path]', diff --git a/src/esbuild/index.ts b/src/esbuild/index.ts index 448da15c..f4bd0cd4 100644 --- a/src/esbuild/index.ts +++ b/src/esbuild/index.ts @@ -237,7 +237,7 @@ export async function runEsbuild( logger.success(format, `⚡️ Build success in ${Math.floor(timeInMs)}ms`) await pluginContainer.buildFinished({ - files: result.outputFiles, + outputFiles: result.outputFiles, metafile: result.metafile, }) } diff --git a/src/lib/report-size.ts b/src/lib/report-size.ts index 3c87fa5c..f121502e 100644 --- a/src/lib/report-size.ts +++ b/src/lib/report-size.ts @@ -2,6 +2,7 @@ import * as colors from 'colorette' import { Logger } from '../log' const prettyBytes = (bytes: number) => { + if (bytes === 0) return '0 B' const unit = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] const exp = Math.floor(Math.log(bytes) / Math.log(1024)) return `${(bytes / Math.pow(1024, exp)).toFixed(2)} ${unit[exp]}` diff --git a/src/options.ts b/src/options.ts index 5be70077..df37a3d8 100644 --- a/src/options.ts +++ b/src/options.ts @@ -58,7 +58,7 @@ export type Options = { [k: string]: string } dts?: boolean | string | DtsConfig - sourcemap?: boolean + sourcemap?: boolean | 'inline' /** Always bundle modules matching given patterns */ noExternal?: (string | RegExp)[] /** Don't bundle these modules */ diff --git a/src/plugin.ts b/src/plugin.ts index 2237826c..c3b5269a 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -41,7 +41,7 @@ export type RenderChunk = ( export type BuildStart = (this: PluginContext) => MaybePromise export type BuildEnd = ( this: PluginContext, - ctx: { metafile?: Metafile } + ctx: { writtenFiles: WrittenFile[] } ) => MaybePromise export type ModifyEsbuildOptions = ( @@ -68,6 +68,8 @@ export type PluginContext = { logger: Logger } +export type WrittenFile = { readonly name: string; readonly size: number } + const parseSourceMap = (map?: string | object | null) => { return typeof map === 'string' ? JSON.parse(map) : map } @@ -109,27 +111,31 @@ export class PluginContainer { } async buildFinished({ - files, + outputFiles, metafile, }: { - files: OutputFile[] + outputFiles: OutputFile[] metafile?: Metafile }) { + const files: Array = outputFiles + .filter((file) => !file.path.endsWith('.map')) + .map((file) => { + if (isJS(file.path) || isCSS(file.path)) { + return { + type: 'chunk', + path: file.path, + code: file.text, + map: outputFiles.find((f) => f.path === `${file.path}.map`)?.text, + } + } else { + return { type: 'asset', path: file.path, contents: file.contents } + } + }) + + const writtenFiles: WrittenFile[] = [] + await Promise.all( - files.map(async (file) => { - const info: AssetInfo | ChunkInfo = - isJS(file.path) || isCSS(file.path) - ? { - type: 'chunk', - path: file.path, - code: file.text, - map: files.find((f) => f.path === `${file.path}.map`)?.text, - } - : { - type: 'asset', - path: file.path, - contents: file.contents, - } + files.map(async (info) => { for (const plugin of this.plugins) { if (info.type === 'chunk' && plugin.renderChunk) { const result = await plugin.renderChunk.call( @@ -157,35 +163,67 @@ export class PluginContainer { } } - await outputFile( - info.path, + const inlineSourceMap = this.context!.options.sourcemap === 'inline' + const contents = info.type === 'chunk' - ? info.code + getSourcemapComment(!!info.map, info.path, isCSS(file.path)) - : info.contents, - { mode: info.type === 'chunk' ? info.mode : undefined } - ) - if (info.type === 'chunk' && info.map) { + ? 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 - // map.sources = map.sources?.map((name: string) => - // path.relative(path.dirname(info.path), name) - // ) - await outputFile(`${info.path}.map`, JSON.stringify(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(), { metafile }) + await plugin.buildEnd.call(this.getContext(), { writtenFiles }) } } } } -const getSourcemapComment = (hasMap: boolean, filepath: string, isCssFile: boolean) => { - if (!hasMap) return '' +const getSourcemapComment = ( + inline: boolean, + map: RawSourceMap | string | null | undefined, + filepath: string, + isCssFile: boolean +) => { + if (!map) return '' const prefix = isCssFile ? '/*' : '//' const suffix = isCssFile ? ' */' : '' - return `${prefix}# sourceMappingURL=${path.basename(filepath)}.map${suffix}` + const url = inline + ? `data:application/json;base64,${Buffer.from( + typeof map === 'string' ? map : JSON.stringify(map) + ).toString('base64')}` + : `${path.basename(filepath)}.map` + return `${prefix}# sourceMappingURL=${url}${suffix}` } diff --git a/src/plugins/size-reporter.ts b/src/plugins/size-reporter.ts index c70d1028..2239ed9f 100644 --- a/src/plugins/size-reporter.ts +++ b/src/plugins/size-reporter.ts @@ -5,15 +5,14 @@ export const sizeReporter = (): Plugin => { return { name: 'size-reporter', - buildEnd({ metafile }) { - if (!metafile) return + buildEnd({ writtenFiles }) { reportSize( this.logger, this.format, - Object.keys(metafile.outputs).reduce((res, name) => { + writtenFiles.reduce((res, file) => { return { ...res, - [name]: metafile!.outputs[name].bytes, + [file.name]: file.size, } }, {}) ) diff --git a/test/index.test.ts b/test/index.test.ts index c2fc109c..2abe9501 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -162,7 +162,7 @@ test('enable --dts-resolve for specific module', async (t) => { }) test('bundle graphql-tools with --sourcemap flag', async (t) => { - await run( + const { outFiles } = await run( t.title, { 'input.ts': `export { makeExecutableSchema } from 'graphql-tools'`, @@ -171,11 +171,11 @@ test('bundle graphql-tools with --sourcemap flag', async (t) => { flags: ['--sourcemap'], } ) - t.pass() + t.deepEqual(outFiles, ['input.js', 'input.js.map']) }) test('bundle graphql-tools with --sourcemap inline flag', async (t) => { - const { output } = await run( + const { output, outFiles } = await run( t.title, { 'input.ts': `export { makeExecutableSchema } from 'graphql-tools'`, @@ -185,7 +185,8 @@ test('bundle graphql-tools with --sourcemap inline flag', async (t) => { } ) - t.assert(output.includes('//# sourceMappingURL=')) + t.assert(output.includes('//# sourceMappingURL=data:application/json;base64')) + t.deepEqual(outFiles, ['input.js']) }) test('multiple formats', async (t) => {