Skip to content

Commit 207dbc7

Browse files
committed
js api: fall back to js-based metafile json parser
1 parent 1ca56dc commit 207dbc7

5 files changed

Lines changed: 444 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@
4646
export { default as 'window.jQuery' } from 'jquery';
4747
```
4848

49+
* Attempt to improve API handling of huge metafiles ([#4329](https://github.com/evanw/esbuild/issues/4329), [#4415](https://github.com/evanw/esbuild/issues/4415))
50+
51+
This release contains a few changes that attempt to improve the behavior of esbuild's JavaScript API with huge metafiles (esbuild's name for the build metadata, formatted as a JSON object). The JavaScript API is designed to return the metafile JSON as a JavaScript object in memory, which makes it easy to access from within a JavaScript-based plugin. Multiple people have encountered issues where this API breaks down with a pathologically-large metafile.
52+
53+
The primary issue is that V8 has an implementation-specific maximum string length, so using the `JSON.parse` API with large enough strings is impossible. This release will now attempt to use a fallback JavaScript-based JSON parser that operates directly on the UTF8-encoded JSON bytes instead of using `JSON.parse` when the JSON metafile is too big to fit in a JavaScript string. The new fallback path has not yet been heavily-tested. The metafile will also now be generated with whitespace removed if the bundle is significantly large, which will reduce the size of the metafile JSON slightly.
54+
55+
However, hitting this case is potentially a sign that something else is wrong. Ideally you wouldn't be building something so enormous that the build metadata can't even fit inside a JavaScript string. You may want to consider optimizing your project, or breaking up your project into multiple parts that are built independently. Another option could potentially be to use esbuild's command-line API instead of its JavaScript API, which is more efficient (although of course then you can't use JavaScript plugins, so it may not be an option).
56+
4957
## 0.27.3
5058

5159
* Preserve URL fragments in data URLs ([#4370](https://github.com/evanw/esbuild/issues/4370))

cmd/esbuild/service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -660,7 +660,7 @@ func (service *serviceType) handleBuildRequest(id uint32, request map[string]int
660660
response["outputFiles"] = encodeOutputFiles(result.OutputFiles)
661661
}
662662
if options.Metafile {
663-
response["metafile"] = result.Metafile
663+
response["metafile"] = []byte(result.Metafile)
664664
}
665665
if options.MangleCache != nil {
666666
response["mangleCache"] = result.MangleCache

lib/shared/common.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type * as types from "./types"
22
import * as protocol from "./stdio_protocol"
3+
import { JSON_parse } from "./uint8array_json_parser"
34

45
declare const ESBUILD_VERSION: string
56

@@ -952,7 +953,7 @@ function buildOrContextImpl(
952953
const originalErrors = result.errors.slice()
953954
const originalWarnings = result.warnings.slice()
954955
if (response!.outputFiles) result.outputFiles = response!.outputFiles.map(convertOutputFiles)
955-
if (response!.metafile) result.metafile = JSON.parse(response!.metafile)
956+
if (response!.metafile) result.metafile = parseJSON(response!.metafile)
956957
if (response!.mangleCache) result.mangleCache = response!.mangleCache
957958
if (response!.writeToStdout !== void 0) console.log(protocol.decodeUTF8(response!.writeToStdout).replace(/\n$/, ''))
958959
runOnEndCallbacks(result, (onEndErrors, onEndWarnings) => {
@@ -1858,3 +1859,20 @@ function jsRegExpToGoRegExp(regexp: RegExp): string {
18581859
if (regexp.flags) result = `(?${regexp.flags})${result}`
18591860
return result
18601861
}
1862+
1863+
function parseJSON(bytes: Uint8Array): any {
1864+
let text: string
1865+
try {
1866+
// This may fail in V8 with the error "Cannot create a string longer than
1867+
// 0x1fffffe8 characters". Other JS engines may have similar limitations.
1868+
text = protocol.decodeUTF8(bytes)
1869+
} catch {
1870+
// In that case, we attempt to parse the JSON ourselves directly from the
1871+
// Uint8Array. This bypasses the string length limit as we no longer need
1872+
// to construct a string that's the length of the input. However, doing
1873+
// this is likely significantly slower (perhaps around ~4x slower?), so we
1874+
// only do it if we have to.
1875+
return JSON_parse(bytes)
1876+
}
1877+
return JSON.parse(text)
1878+
}

lib/shared/stdio_protocol.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export interface BuildResponse {
5151
errors: types.Message[]
5252
warnings: types.Message[]
5353
outputFiles?: BuildOutputFile[]
54-
metafile?: string
54+
metafile?: Uint8Array
5555
mangleCache?: Record<string, string | false>
5656
writeToStdout?: Uint8Array
5757
}

0 commit comments

Comments
 (0)