diff --git a/CHANGELOG.md b/CHANGELOG.md index 3da84ddc0..8c6a0884f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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**: `` 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..fab7dd641 100644 --- a/context/workarounds/bun-issues.md +++ b/context/workarounds/bun-issues.md @@ -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) diff --git a/devenv.nix b/devenv.nix index 600f690ca..8b5591afd 100644 --- a/devenv.nix +++ b/devenv.nix @@ -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 { @@ -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 = [ @@ -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 diff --git a/flake.nix b/flake.nix index f06399b54..24d31c04e 100644 --- a/flake.nix +++ b/flake.nix @@ -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; @@ -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; @@ -147,6 +154,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"; + }; + } + ); +} diff --git a/packages/@overeng/tui-stories/src/StoryDiscovery.ts b/packages/@overeng/tui-stories/src/StoryDiscovery.ts index ecba7d419..d47c05953 100644 --- a/packages/@overeng/tui-stories/src/StoryDiscovery.ts +++ b/packages/@overeng/tui-stories/src/StoryDiscovery.ts @@ -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[] @@ -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) 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: {