Skip to content

SSG stuff - #20998

Merged
Jarred-Sumner merged 64 commits into
mainfrom
zack/ssg-2
Jul 21, 2025
Merged

SSG stuff#20998
Jarred-Sumner merged 64 commits into
mainfrom
zack/ssg-2

Conversation

@zackradisic

Copy link
Copy Markdown
Contributor

What does this PR do?

Various changes and bug fixes for SSG:

  1. fix assertion failure: make sure chunk[i] corresponds to output_files[i] (previously we were pushing sourcemaps and bytecode files and breaking the order, see OutputFileListBuilder)
  2. enable sourcemaps for SSG
  3. fix bug with error codepath causing bake to reference undefined stack memory
  4. fix import.meta.{dirname,dir,url,file} breaking in SSG (inline the values in js_parser.zig)
  5. support catch-all routes in SSG
  6. add some configuration for bundler stuff for ssg
  7. fix bug where the __esm and __commonJS wrapper functions weren't being written to disk and were breaking everything
  8. detect if a route is fully static and don't include the client react runtime stuff if so
  9. fix bug where default export of a use client component was not being found
  10. fix crash when running ssg with no routes (lol)
  11. fix bug where doing import Foo, * as from './module' would not be able to find the default export
  12. detect if useState is used inside of a server component and throw helpful error message
  13. make 'use client' work

Comment thread src/bake/production.zig Resolved
Comment thread src/bake/production.zig
const runtime_file: *const OutputFile = &bundled_outputs[runtime_file_index];
_ = runtime_file.writeToDisk(root_dir, ".") catch |err| {
bun.handleErrorReturnTrace(err, @errorReturnTrace());
Output.err(err, "Failed to write {} to output directory", .{bun.fmt.quote(runtime_file.dest_path)});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this fail the build instead of logging to stderr? Should this be using a logger.Log?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the runtime file is not written to disk then a lot things will break so I think it is better to fail it now

Comment thread src/bake/production.zig Resolved
Comment thread src/js/builtins/Bake.ts
* as well as use the async primitives provided by the language.
*/
export function renderRoutesForProdStatic(
export async function renderRoutesForProdStatic(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Async creates microtasks. Do we need it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to sequentially load the page modules with await to prevent annoying bugs with concurrently loading and calling esm modules so yeah

Comment thread src/js/builtins/Bake.ts Resolved
Comment thread src/js/builtins/Bake.ts Resolved
Comment thread src/js_parser.zig Resolved
Comment thread src/js_parser.zig Resolved
Comment thread src/sourcemap/sourcemap.zig Resolved
Comment thread src/bun.js/bindings/ZigGlobalObject.cpp Resolved
@Jarred-Sumner
Jarred-Sumner merged commit f4339df into main Jul 21, 2025
@Jarred-Sumner
Jarred-Sumner deleted the zack/ssg-2 branch July 21, 2025 05:37
Jarred-Sumner pushed a commit that referenced this pull request Sep 29, 2026
### Problem

- An assert-enabled build (debug, ASAN) aborts with `panic: assertion
failed: self.cap > 0` in `StringBuilder::writable()`. `bun install`
reaches it with a hosted-git shortcut that ends at the colon, like
`"overrides": { "zz": "github:" }`.
- `allocate()` stores a pointer also for a count of 0 bytes.
`writable()` and `allocated_slice()`
(`src/bun_core/string/StringBuilder.rs:201-226`) asserted `cap > 0` for
each builder with a pointer.

### Fix

- `writable()` asserts `len <= cap`, the precondition of its pointer
arithmetic. This removes the abort.
- `allocated_slice()` loses the same assert. The Postgres call site
drops its #22558 guard and reads `written_slice()`. No path on main
fails there.
- Verified: `test/cli/install/hosted-git-info/`. On a debug build of
main, 5 install tests and the guard of 13 parser rows fail (timeouts).
All 18 pass with the fix. Two unit tests cover both views (Miri).
- Self-reviewed: 2 concerns raised, 2 addressed.

### Background

- `StringBuilder` counts each slice, allocates one buffer, then appends
into it.
- `HostedGitInfo` mirrors npm's hosted-git-info. Both return `project:
""` for `github:`. The tests record that output, not a new contract.
- Considered a guard in `HostedGitInfo::copy_from`. It adds a release
branch and leaves the assert false. Bake `computeChunks` (#20998) and
Postgres `ErrorResponse` (#22558) hit it before.

### Downsides

- Assert-enabled builds lose the `cap > 0` check. A builder that counted
nothing and appends bytes still panics on the bounds check.
- Release: 0 bytes changed in 4 `StringBuilder` symbols and 3 callers.
Only `error_response_jsc::to_js` changes: 4412 -> 4400 B.

<details><summary>Notes</summary>

**Origin.** A fuzzer found the abort on an assert-enabled build. No
issue exists. Only linux x64 was run.

**Proof runs (debug build).**

- Source of main (a4f1429): 5 of 5 new install tests fail. The
`beforeAll` hook of the 13 parser rows fails too, so these rows do not
run. Each failure shows `timed out after 5000ms`, not an assertion diff.
The child that aborts needs 5 s or more to print its stack trace. The
late output has `panic: assertion failed: self.cap > 0`.
- This PR: 18 of 18 pass.
- The new tests pass on a release build of main, because a release build
has no assert. `USE_SYSTEM_BUN=1` cannot show the failure.
-
`test/regression/issue/postgres-stringbuilder-assertion-aggressive.test.ts`
passes on main and on this PR. It does not tell the two apart.
- No caller reaches `allocated_slice()` with a zero-capacity buffer now.
The Postgres call site was the one caller that could, and it reads
`written_slice()`. `allocated_slice()` makes a slice over the whole
capacity, and only the first `len` bytes are written.
- Two unit tests in `StringBuilder.rs` take an empty builder through
`writable()`, `allocated_slice()`, `append()`, `written_slice()` and
`move_to_slice()`. `cargo miri test -p bun_core --lib string_builder`
runs them: 2 pass. With one of the old asserts back, 2 fail. CI does not
run them. `cargo test -p bun_core` does not link, and the Miri job does
not list `bun_core`, because other tests of that crate call foreign
functions.

**Measurements.** Release builds of main (a4f1429) and of this PR,
same toolchain.

- release code, 4 StringBuilder symbols and 3 inlining callers: 0 bytes
changed, 0 instructions differ (objdump). The symbols are
`StringBuilder::allocate`, `append`, `append_count`, `add_concat`,
`HostedGitInfo::from_url`, `HTTPClient::clone_metadata` and
`bun_picohttp::Response::clone`.
- release .text: +0 B (readelf, 58,143,477 B on main and on the PR).
`error_response_jsc::to_js`: 4412 -> 4400 B. It is the only one of
78,610 text symbols that changes size.
- from_url instructions per call (`github:user/repo#main`): 2596 -> 2596
(gdb `stepi` count over the call, JIT off). valgrind is not installed in
the container.
- allocator calls per from_url call: malloc 7, zalloc 1, realloc 0, free
7 on PR (main: 7, 1, 0, 7).
- syscalls, `bun install` on the override fixture: file 67 -> 67, read
18 -> 18, write 3 -> 3, network 0 -> 0 (gdb `catch syscall`). strace is
not installed in the container.
- assert-enabled checks per view call: `allocated_slice` 1 -> 0,
`writable` 1 -> 1.
- `len <= cap` in `writable()`: 0 assertion failures in 8 existing test
files on the debug build. The files are the three in
`test/cli/install/hosted-git-info/`,
`test/cli/install/overrides.test.ts`,
`test/bundler/bundler_compile.test.ts`,
`test/js/web/fetch/fetch.test.ts`, `test/js/bun/spawn/spawn.test.ts` and
the Postgres test above. `fetch.test.ts` has the same 25 failures on
main and on the PR. On the PR run, 2 more tests of
`bundler_compile.test.ts` and 4 more tests of `spawn.test.ts` reached
the 5000 ms timeout. All 6 pass with a longer timeout. The host had a
load average above 250 during the runs.

**Callers.** `bun_core::StringBuilder` has 27 construction sites in 18
files: 19 direct and 8 through `HeaderBuilder::default()`. `writable()`
has 10 callers outside the builder. Each one indexes the returned slice
with bounds checks and adds to `len` only what it wrote. So `len <= cap`
holds for them.

**Sites left as they are.**

- `src/runtime/valkey_jsc/js_valkey.rs:657-675` and
`src/bundler/linker_context/computeChunks.rs:551-555` skip the builder
when there is nothing to build. They also skip work, so they stay.
- Open PR #43942 sizes a buffer above the exact sum because of this
assert. It can use the exact sum after this PR.

**Tests.**

- The install tests are in
`hosted-git-info/boundary-conditions.test.ts`, not in
`overrides.test.ts`. That file installs from the public npm registry,
and these tests are about the spec parser.
- The `dependency github:` test records what release bun does for that
spec today. It sends one `GET /repos//tarball/` and exits with code 1.
The test uses a local server through `GITHUB_API_URL`.
- The expected objects in `from-url.test.ts` are the output of
hosted-git-info 9.0.0 on node 26.3.0 for the same 13 inputs.
- The 13 parser rows call the parser in the test process. A `beforeAll`
hook runs `bun install` with the override `github:` in a child process
first. An abort in the parser fails the hook, and the rows do not run. A
child process that imports `bun:internal-for-testing` needs 2 s to 5 s
on a debug build under load. That is too near the 5000 ms default.
- The `bun.lock` test compares each stderr line that names the lockfile.
`bun install` prints `Ignoring lockfile` and continues when it cannot
load `bun.lock`.
- One `bun install` covers the override values of all five providers.

**Differences from hosted-git-info 9.0.0 that this PR does not change.**

- `github:user/repo%2Egit`: the reference returns project `repo`, bun
returns `repo.git`.
- `github:%`: the reference returns `undefined`, `hostedGitInfo.fromUrl`
in bun throws `Invalid Git URL: InvalidURL`.
- `github:/.git`: the reference returns project `""`, bun returns `git`.
`new URL("github:/.git").pathname` is `git` in bun. Open PR #39457
covers that.

</details>

<!-- robobun:evidence:begin -->

---

**[human-review]** gate passed · iteration 1 · 4 files touched

<details><summary>fails on main (without fix)</summary>

```console
ASAN without fix: 6 failed, 32 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/cli/install/hosted-git-info/boundary-conditions.test.ts test/cli/install/hosted-git-info/from-url.test.ts
bun test v1.4.3 (367d939)

test/cli/install/hosted-git-info/from-url.test.ts:
killed 1 dangling process
(fail) fromUrl > shortcuts with an empty user, project and committish > (unnamed) [5008.49ms]
  ^ a beforeEach/afterEach hook timed out for this test.
(pass) fromUrl > valid urls > bitbucket > parses bitbucket:foo/bar [11.72ms]
(pass) fromUrl > valid urls > bitbucket > parses bitbucket:foo/bar#branch [4.62ms]
(pass) fromUrl > valid urls > bitbucket > parses bitbucket:user@foo/bar [3.28ms]
(pass) fromUrl > valid urls > bitbucket > parses bitbucket:user@foo/bar#branch [1.22ms]
(pass) fromUrl > valid urls > bitbucket > parses bitbucket:user:password@foo/bar [1.26ms]
(pass) fromUrl > valid urls > bitbucket > parses bitbucket:user:password@foo/bar#branch [3.13ms]
(pass) fromUrl > valid urls > bitbucket > parses bitbucket::password@foo/bar [0.94ms]
(pass) fromUrl > valid urls > bitbucket > parses bitbucket::password@foo/bar#branc
... (truncated)

release without fix: 32 skipped
bun test v1.4.3-canary.1 (6780601)

test/cli/install/hosted-git-info/from-url.test.ts:
(pass) fromUrl > shortcuts with an empty user, project and committish > parses github: [2.39ms]
(pass) fromUrl > shortcuts with an empty user, project and committish > parses gitlab: [0.07ms]
(pass) fromUrl > shortcuts with an empty user, project and committish > parses bitbucket:
(pass) fromUrl > shortcuts with an empty user, project and committish > parses gist:
(pass) fromUrl > shortcuts with an empty user, project and committish > parses sourcehut: [1.02ms]
(pass) fromUrl > shortcuts with an empty user, project and committish > parses github:.git [0.01ms]
(pass) fromUrl > shortcuts with an empty user, project and committish > parses github:/
(pass) fromUrl > shortcuts with an empty user, project and committish > parses github:// [0.01ms]
(pass) fromUrl > shortcuts with an empty user, project and committish > parses github:#
(pass) fromUrl > shortcuts with an empty user, project and committish > parses github:?x
(pass) fromUrl > shortcuts with an empty user, project and committish > parses github:@
(pass) fromUrl > shortcuts with an empty user, project and committish > parses
... (truncated)
```

</details>

<details><summary>passes on PR (with fix)</summary>

```console
ASAN with fix: 32 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/cli/install/hosted-git-info/boundary-conditions.test.ts test/cli/install/hosted-git-info/from-url.test.ts
bun test v1.4.3 (367d939)

test/cli/install/hosted-git-info/from-url.test.ts:
(pass) fromUrl > shortcuts with an empty user, project and committish > parses github: [4.28ms]
(pass) fromUrl > shortcuts with an empty user, project and committish > parses gitlab: [1.19ms]
(pass) fromUrl > shortcuts with an empty user, project and committish > parses bitbucket: [0.91ms]
(pass) fromUrl > shortcuts with an empty user, project and committish > parses gist: [0.75ms]
(pass) fromUrl > shortcuts with an empty user, project and committish > parses sourcehut: [0.82ms]
(pass) fromUrl > shortcuts with an empty user, project and committish > parses github:.git [1.52ms]
(pass) fromUrl > shortcuts with an empty user, project and committish > parses github:/ [2.71ms]
(pass) fromUrl > shortcuts with an empty user, project and committish > parses github:// [0.98ms]
(pass) fromUrl > shortcuts with an empty user, project and committish > parses git
... (truncated)

release with fix: 32 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 6641ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/89] build.rs build_script_build
[2/89] rustc bun_platform 
[3/89] rustc bun_core 
[4/89] rustc bun_boringssl_sys 
[5/89] rustc bun_safety 
[6/89] rustc bun_cares_sys 
[7/89] rustc bun_zlib_sys 
[8/89] rustc bun_errno 
[9/89] rustc bun_picohttp 
[10/89] rustc bun_brotli 
[11/89] rustc bun_zstd 
[12/89] rustc bun_base64 
[13/89] rustc bun_ptr 
[14/89] rustc bun_output 
[15/89] rustc bun_valkey 
[16/89] rustc bun_clap 
[17/89] rustc bun_lsquic_sys 
[18/89] rustc bun_paths 
[19/89] rustc bun_tcc_sys 
[20/89] rustc bun_collections 
[21/89] rustc bun_shell_parser 
warning: `feature(generic_const_exprs)` is not supported with the next-generation trait solver
 --> src/shell_parser/lib.rs:1:30
  |
1 | #![feature(adt_const_params, generic_const_exprs, allocator_api)]
  |                              ^^^^^^^^^^^^^^^^^^^
  |
  = note: `-Znext-solver=globally` is currently enabled by default for testing
  = note: reverted the setting to `-Znext-solver=coherence` for this crate
  = note: the currently stable trait s
... (truncated)
```

</details>

<details><summary>diff hotspot</summary>

```
src/bun_core/string/StringBuilder.rs               | 30 +++++++-
 .../postgres/protocol/error_response_jsc.rs        |  8 +-
 .../hosted-git-info/boundary-conditions.test.ts    | 89 ++++++++++++++++++++++
 test/cli/install/hosted-git-info/from-url.test.ts  | 47 +++++++++++-
 4 files changed, 164 insertions(+), 10 deletions(-)
```

</details>

**gate history** · 1 passed · 0 rejected · iteration 1

<details><summary>evidence per changed file</summary>

```
file                                                      reads  edits  tests
src/bun_core/string/StringBuilder.rs                          2      1     35
src/sql_jsc/postgres/protocol/error_response_jsc.rs           2      2     27
…cli/install/hosted-git-info/boundary-conditions.test.ts      5      4     20
test/cli/install/hosted-git-info/from-url.test.ts             6      6     24
```

</details>

<!-- robobun:evidence:end -->
robobun added a commit that referenced this pull request Sep 29, 2026
JSC::VM::tryCreate can return null. Zig__GlobalObject__create (#16068) and
BakeCreateProdGlobal (#20998) check the result and panic with "Failed to
allocate JavaScriptCore Virtual Machine". vmForBytecodeCache got the
tryCreate call in #16068 without the check, and dereferences the result.

Bun::createVM holds the call, the check and the message, and returns a
Ref<JSC::VM>. The three sites call it.

No report of the null result exists, and no test input produces it. With a
debugger that makes the allocation of the first BigInt constant in the VM
constructor fail, `bun build --bytecode` ends with "Segmentation fault at
address 0x0" before this change and with the panic message after it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants