Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ All notable changes to this project will be documented in this file.

### Changed

- **nix/bun**: Overlay nixpkgs' Bun package to `1.3.14-canary.1+ca9e0896c` with fixed-output hashes so repo builds use the module-loader rewrite that fixes concurrent dynamic import TDZ failures before a stable `bun-v1.3.14` release asset is available.
- **@overeng/tui-stories**: Restore concurrent story module imports on Bun `1.3.14+` and non-Bun runtimes now that Bun fixed the dynamic `import()` TDZ bug in oven-sh/bun#29393; older Bun versions keep the sequential compatibility path.
- **@overeng/notion-react**: `<Page>` and `<ChildPage>` accept `icon={null}` and `cover={null}` as explicit clear sentinels (#618). Dropping the prop is still "no claim" (preserves server state); passing `null` emits `pages.update({icon: null})` / `pages.update({cover: null})`. On a fresh page with no prior icon/cover, `null` is a no-op.
- **@overeng/notion-react**: Same-parent `<ChildPage>` creates are now sequential — JSX order is preserved 1:1 on the server (#618). Parallel `pages.create` under a common parent yields nondeterministic `child_page` ordering; the driver issues sequential POSTs so no post-create re-fetch is needed. T08 (formerly "concurrent sibling-page order is not authoritative") is now a normative invariant; the deferred `ensureSiblingOrder` sync option is dropped.
- **@overeng/notion-react**: `CACHE_SCHEMA_VERSION` bumped `2 → 3` to accommodate per-page cache subtrees (#618). v2 caches fall through the existing `"schema-mismatch"` cold path — transparent, no caller action required. The first sync after upgrade may emit one spurious metadata update per sub-page as response-normalized title/icon/cover is recomputed.
Expand Down
11 changes: 11 additions & 0 deletions context/workarounds/bun-issues.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ This aligns with our pnpm pattern - see "Bun Workspace Pattern" section below.

## Additional Blocking Issues

### Resolved Runtime Issues

#### BUN-04: Concurrent dynamic import TDZ

- [Concurrent dynamic import with top-level await triggers temporal dead zone](https://github.com/oven-sh/bun/issues/20489)
- Fixed by [oven-sh/bun#29393](https://github.com/oven-sh/bun/pull/29393), shipped in Bun `1.3.14`.

`effect-utils` overlays nixpkgs' Bun package to `1.3.14-canary.1+ca9e0896c` while no stable `bun-v1.3.14` release asset exists. The overlay uses fixed-output hashes for the GitHub canary release assets.

`@overeng/tui-stories` keeps a runtime compatibility gate for downstreams that provide their own Bun: Bun `<1.3.14` imports story modules sequentially; Bun `>=1.3.14` and non-Bun runtimes import concurrently.

### BUN-03: Bun patchedDependencies bug

- [Patching falls over when using local path dependencies](https://github.com/oven-sh/bun/issues/13531)
Expand Down
12 changes: 9 additions & 3 deletions devenv.nix
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@
}:
let
repoFlake = builtins.getFlake (toString ./.);
flakePkgs = import repoFlake.inputs.nixpkgs { inherit (pkgs) system; };
# TODO: Drop this overlay once nixpkgs ships a stable Bun version with
# oven-sh/bun#29393 (>= 1.3.14).
bunOverlay = import ./nix/bun-overlay.nix;
flakePkgs = import repoFlake.inputs.nixpkgs {
inherit (pkgs) system;
overlays = [ bunOverlay ];
};
cliBuildStamp = import ./nix/workspace-tools/lib/cli-build-stamp.nix { inherit pkgs; };
# Use npm oxlint with NAPI bindings to enable JavaScript plugin support
oxlintNpm = import ./nix/oxlint-npm.nix {
Expand Down Expand Up @@ -42,7 +48,7 @@ let
context = ./nix/devenv-modules/tasks/shared/context.nix;
};
# Use bun source entrypoints for in-repo CLIs in devenv (flake builds stay strict).
mkSourceCli = import ./nix/devenv-modules/lib/mk-source-cli.nix { inherit pkgs; };
mkSourceCli = import ./nix/devenv-modules/lib/mk-source-cli.nix { pkgs = flakePkgs; };

# CLI packages built with Nix (for hash management)
nixCliPackages = [
Expand Down Expand Up @@ -371,7 +377,7 @@ in
inputs.tsgo.packages.${pkgs.system}.effect-tsgo
(import ./nix/pnpm.nix { inherit pkgs; })
pkgs.nodejs_24
pkgs.bun
flakePkgs.bun
pkgs.typescript
pkgs.flock # Cross-process locking for setup tasks (see setup.nix)
oxlintWithPlugins
Expand Down
11 changes: 10 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,17 @@
# lastModified is the git commit timestamp (Unix seconds)
commitTs = self.sourceInfo.lastModified or 0;
dirty = self.sourceInfo ? dirtyShortRev;
# TODO: Drop this overlay once nixpkgs ships a stable Bun version with
# oven-sh/bun#29393 (>= 1.3.14).
bunOverlay = import ./nix/bun-overlay.nix;
in
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = import nixpkgs { inherit system; };
pkgs = import nixpkgs {
inherit system;
overlays = [ bunOverlay ];
};
mkBunCli = import ./nix/workspace-tools/lib/mk-bun-cli.nix { inherit pkgs; };
cliBuildStamp = import ./nix/workspace-tools/lib/cli-build-stamp.nix { inherit pkgs; };
rootPath = self.outPath;
Expand Down Expand Up @@ -105,6 +111,7 @@
in
{
packages = cliPackages // {
bun = pkgs.bun;
cli-build-stamp = cliBuildStamp.package;
effect-tsgo = tsgo.packages.${system}.effect-tsgo;
genie-dirty = cliPackagesDirty.genie;
Expand Down Expand Up @@ -147,6 +154,8 @@
}
)
// {
overlays.default = bunOverlay;

# Devenv modules for importing into other repos
devenvModules = {
# `dt` command wrapper for devenv tasks with shell completions
Expand Down
41 changes: 41 additions & 0 deletions nix/bun-overlay.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
final: prev:
let
version = "1.3.14-canary.1+ca9e0896c";

canarySource =
system: hash:
final.fetchurl {
# Bun has not published a stable bun-v1.3.14 release asset yet. The
# canary release channel is mutable upstream, but the fixed-output hash
# pins the exact post-module-loader-rewrite binary we want here.
url = "https://github.com/oven-sh/bun/releases/download/canary/bun-${system}.zip";
inherit hash;
};
in
{
bun = prev.bun.overrideAttrs (
finalAttrs: prevAttrs: {
inherit version;

src =
finalAttrs.passthru.sources.${prev.stdenvNoCC.hostPlatform.system}
or (throw "Unsupported system: ${prev.stdenvNoCC.hostPlatform.system}");

passthru = prevAttrs.passthru // {
sources = {
"aarch64-darwin" =
canarySource "darwin-aarch64" "sha256-CvRCXIsSTDxEznoCwT04SDGD+GapVBazNOVyVSKuPYA=";
"aarch64-linux" =
canarySource "linux-aarch64" "sha256-0ys2Rh1g5rHvXy4X/XNWufIorsn5sVqT1sJathUtBFo=";
"x86_64-darwin" =
canarySource "darwin-x64-baseline" "sha256-yScvX5uhuUPWVz6yAwchiqdaFty/em4rc17p4xLxg1s=";
"x86_64-linux" = canarySource "linux-x64" "sha256-Adt21Dh4wMhI7rCEdgp83xKzY4gUAOjeFkc73EaaJlA=";
};
};

meta = prevAttrs.meta // {
changelog = "https://github.com/oven-sh/bun/compare/af24e281e...ca9e0896c";
};
}
);
}
30 changes: 23 additions & 7 deletions packages/@overeng/tui-stories/src/StoryDiscovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,28 @@ export interface DiscoverStoriesResult {
readonly skippedCount: number
}

type StoryImportConcurrency = 1 | 'unbounded'

const bunVersionSupportsConcurrentDynamicImport = (version: string): boolean => {
const [major = 0, minor = 0, patch = 0] = version
.split('.')
.map((part) => Number.parseInt(part, 10))
.map((part) => (Number.isNaN(part) === true ? 0 : part))

return major > 1 || (major === 1 && (minor > 3 || (minor === 3 && patch >= 14)))
}

/** Return safe story import concurrency for the active JavaScript runtime. */
export const storyImportConcurrencyForRuntime = (
bunVersion: string | undefined = process.versions.bun,
): StoryImportConcurrency => {
if (bunVersion === undefined) return 'unbounded'

// Fixed by Bun's module-loader rewrite in 1.3.14:
// https://github.com/oven-sh/bun/issues/20489
return bunVersionSupportsConcurrentDynamicImport(bunVersion) === true ? 'unbounded' : 1
}

/** Discover and parse all story files in the given package directories */
export const discoverStories = (options: {
readonly packageDirs: readonly string[]
Expand All @@ -84,15 +106,9 @@ export const discoverStories = (options: {
return { modules: [], skippedCount: 0 }
}

/* Sequential imports to avoid Bun's ESM TDZ bug: when concurrent import() calls
share a dependency and one chain fails (e.g. missing module), Bun leaves the shared
module's bindings uninitialized for other importers. With the shared
@overeng/tui-react/storybook dependency this caused ~100% TDZ failure rate.
Performance is unaffected — shared modules are cached after first evaluation.
See: https://github.com/oven-sh/bun/issues/20489 */
const results = yield* Effect.all(
filePaths.map((fp) => importStoryFile(fp)),
{ concurrency: 1 },
{ concurrency: storyImportConcurrencyForRuntime() },
)

const modules = results.filter((m): m is ParsedStoryModule => m !== undefined)
Expand Down
9 changes: 8 additions & 1 deletion packages/@overeng/tui-stories/test/StoryDiscovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { resolve } from 'node:path'
import { describe, it, expect } from '@effect/vitest'
import { Effect } from 'effect'

import { discoverStories } from '../src/StoryDiscovery.ts'
import { discoverStories, storyImportConcurrencyForRuntime } from '../src/StoryDiscovery.ts'
import { parseStoryModule } from '../src/StoryModule.ts'

const WORKSPACE_ROOT = resolve(import.meta.dirname, '../../../..')
Expand Down Expand Up @@ -39,6 +39,13 @@ describe('StoryDiscovery', () => {
}),
)

it('uses concurrent story imports outside affected Bun versions', () => {
expect(storyImportConcurrencyForRuntime(undefined)).toBe('unbounded')
expect(storyImportConcurrencyForRuntime('1.3.13')).toBe(1)
expect(storyImportConcurrencyForRuntime('1.3.14')).toBe('unbounded')
expect(storyImportConcurrencyForRuntime('1.4.0')).toBe('unbounded')
})

it('parseStoryModule handles valid exports', () => {
const mod = parseStoryModule({
exports: {
Expand Down
Loading