From 54ad71a752ab1ffadaec172c78b92b98493398cb Mon Sep 17 00:00:00 2001 From: schickling-assistant <261620128+schickling-assistant@users.noreply.github.com> Date: Tue, 28 Apr 2026 11:59:34 +0200 Subject: [PATCH 1/4] Gate tui story import workaround by Bun version --- CHANGELOG.md | 1 + context/workarounds/bun-issues.md | 9 ++++++ .../tui-stories/src/StoryDiscovery.ts | 29 ++++++++++++++----- .../tui-stories/test/StoryDiscovery.test.ts | 9 +++++- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3da84ddc0..8fe537559 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ All notable changes to this project will be documented in this file. ### Changed +- **@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**: `` and `` 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 `` 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. diff --git a/context/workarounds/bun-issues.md b/context/workarounds/bun-issues.md index 95cfbc002..c26cd3afc 100644 --- a/context/workarounds/bun-issues.md +++ b/context/workarounds/bun-issues.md @@ -56,6 +56,15 @@ 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`. + +`@overeng/tui-stories` keeps a runtime compatibility gate: 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) diff --git a/packages/@overeng/tui-stories/src/StoryDiscovery.ts b/packages/@overeng/tui-stories/src/StoryDiscovery.ts index ecba7d419..01bdd1d2e 100644 --- a/packages/@overeng/tui-stories/src/StoryDiscovery.ts +++ b/packages/@overeng/tui-stories/src/StoryDiscovery.ts @@ -70,6 +70,27 @@ 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) ? 0 : part)) + + return major > 1 || (major === 1 && (minor > 3 || (minor === 3 && patch >= 14))) +} + +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) ? 'unbounded' : 1 +} + /** Discover and parse all story files in the given package directories */ export const discoverStories = (options: { readonly packageDirs: readonly string[] @@ -84,15 +105,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) diff --git a/packages/@overeng/tui-stories/test/StoryDiscovery.test.ts b/packages/@overeng/tui-stories/test/StoryDiscovery.test.ts index 142af770d..0fe7299b1 100644 --- a/packages/@overeng/tui-stories/test/StoryDiscovery.test.ts +++ b/packages/@overeng/tui-stories/test/StoryDiscovery.test.ts @@ -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, '../../../..') @@ -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: { From 5f3b788824b94480ca98c61f4c5d603b316d83dc Mon Sep 17 00:00:00 2001 From: schickling-assistant <261620128+schickling-assistant@users.noreply.github.com> Date: Tue, 28 Apr 2026 12:12:53 +0200 Subject: [PATCH 2/4] Overlay Bun with fixed module loader build --- CHANGELOG.md | 1 + context/workarounds/bun-issues.md | 4 ++- devenv.nix | 10 +++++--- flake.nix | 9 ++++++- nix/bun-overlay.nix | 41 +++++++++++++++++++++++++++++++ 5 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 nix/bun-overlay.nix diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fe537559..8c6a0884f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ 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**: `` and `` 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 `` 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. diff --git a/context/workarounds/bun-issues.md b/context/workarounds/bun-issues.md index c26cd3afc..fab7dd641 100644 --- a/context/workarounds/bun-issues.md +++ b/context/workarounds/bun-issues.md @@ -63,7 +63,9 @@ This aligns with our pnpm pattern - see "Bun Workspace Pattern" section below. - [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`. -`@overeng/tui-stories` keeps a runtime compatibility gate: Bun `<1.3.14` imports story modules sequentially; Bun `>=1.3.14` and non-Bun runtimes import concurrently. +`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 diff --git a/devenv.nix b/devenv.nix index 600f690ca..bc3f966a0 100644 --- a/devenv.nix +++ b/devenv.nix @@ -7,7 +7,11 @@ }: let repoFlake = builtins.getFlake (toString ./.); - flakePkgs = import repoFlake.inputs.nixpkgs { inherit (pkgs) system; }; + 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 { @@ -42,7 +46,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 = [ @@ -371,7 +375,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 diff --git a/flake.nix b/flake.nix index f06399b54..9e7223dc6 100644 --- a/flake.nix +++ b/flake.nix @@ -29,11 +29,15 @@ # lastModified is the git commit timestamp (Unix seconds) commitTs = self.sourceInfo.lastModified or 0; dirty = self.sourceInfo ? dirtyShortRev; + 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; @@ -105,6 +109,7 @@ in { packages = cliPackages // { + bun = pkgs.bun; cli-build-stamp = cliBuildStamp.package; effect-tsgo = tsgo.packages.${system}.effect-tsgo; genie-dirty = cliPackagesDirty.genie; @@ -147,6 +152,8 @@ } ) // { + overlays.default = bunOverlay; + # Devenv modules for importing into other repos devenvModules = { # `dt` command wrapper for devenv tasks with shell completions diff --git a/nix/bun-overlay.nix b/nix/bun-overlay.nix new file mode 100644 index 000000000..90571bb27 --- /dev/null +++ b/nix/bun-overlay.nix @@ -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"; + }; + } + ); +} From 24b0d1a007411ef8c3fccf60bfd1ce7d8a496ea0 Mon Sep 17 00:00:00 2001 From: schickling-assistant <261620128+schickling-assistant@users.noreply.github.com> Date: Tue, 28 Apr 2026 12:26:55 +0200 Subject: [PATCH 3/4] Fix tui stories lint warnings --- packages/@overeng/tui-stories/src/StoryDiscovery.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/@overeng/tui-stories/src/StoryDiscovery.ts b/packages/@overeng/tui-stories/src/StoryDiscovery.ts index 01bdd1d2e..d47c05953 100644 --- a/packages/@overeng/tui-stories/src/StoryDiscovery.ts +++ b/packages/@overeng/tui-stories/src/StoryDiscovery.ts @@ -76,11 +76,12 @@ 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) ? 0 : part)) + .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 => { @@ -88,7 +89,7 @@ export const storyImportConcurrencyForRuntime = ( // Fixed by Bun's module-loader rewrite in 1.3.14: // https://github.com/oven-sh/bun/issues/20489 - return bunVersionSupportsConcurrentDynamicImport(bunVersion) ? 'unbounded' : 1 + return bunVersionSupportsConcurrentDynamicImport(bunVersion) === true ? 'unbounded' : 1 } /** Discover and parse all story files in the given package directories */ From 15eeb275a36ddfa70bbe12711fde681966cc4d47 Mon Sep 17 00:00:00 2001 From: schickling-assistant <261620128+schickling-assistant@users.noreply.github.com> Date: Tue, 28 Apr 2026 12:28:17 +0200 Subject: [PATCH 4/4] Document temporary Bun overlay --- devenv.nix | 2 ++ flake.nix | 2 ++ 2 files changed, 4 insertions(+) diff --git a/devenv.nix b/devenv.nix index bc3f966a0..8b5591afd 100644 --- a/devenv.nix +++ b/devenv.nix @@ -7,6 +7,8 @@ }: let repoFlake = builtins.getFlake (toString ./.); + # 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; diff --git a/flake.nix b/flake.nix index 9e7223dc6..24d31c04e 100644 --- a/flake.nix +++ b/flake.nix @@ -29,6 +29,8 @@ # 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 (