Skip to content

Move export declarations to bottom of module files - #58

Merged
rafaeelricco merged 2 commits into
mainfrom
update-exports
May 12, 2026
Merged

Move export declarations to bottom of module files#58
rafaeelricco merged 2 commits into
mainfrom
update-exports

Conversation

@rafaeelricco

Copy link
Copy Markdown
Contributor

Motivation

The mobile app in HartAgency crashes on sign-in with TypeError: Cannot read property 'object' of undefined, thrown from @ambarltd/core/dist/json/schema.js at module init. The compiled file lists its aggregate export { ... } clause ahead of the namespace imports it re-exports, and ./decoder / ./encoder are each imported twice (once as decoder / encoder, once as D / E). Node's ESM loader links exports before any top-level code runs, so it survives this ordering. Metro lowers each module to a CJS-style factory and hoists the export object; under that transform the second namespace alias (D) resolves to undefined when D.object(pdef) executes. Reordering the exports to the bottom and dropping the duplicate aliases removes the trigger so the same @ambarltd/core build runs cleanly under Metro, Node, esbuild, Rollup, and Webpack 5. Full write-up: docs/ambar-core-metro-bug.md in the HartAgency repo.

What's New

Export Ordering Refactor

  • Move the aggregate export { ... } block from the top of every module file to the bottom in core, tasks, task-explorer/backend, and task-explorer/frontend
  • Eliminates the export-before-import pattern that Metro's hoist-and-dedup pass cannot lower correctly

json/schema.ts Namespace Cleanup

  • Drop the duplicate import * as D from "./decoder" and import * as E from "./encoder" aliases
  • Rewrite every D.* / E.* reference to use the existing decoder.* / encoder.* namespaces
  • Import DecoderOptional and EncoderOptional as named types in place of D.DecoderOptional / E.EncoderOptional
  • Inline const decoder = ...; const encoder = ...; return new Schema(decoder, encoder) in object, pair, and triple so the local bindings no longer shadow the module-scope namespace imports

Release

  • Bump @ambarltd/core to 0.1.15 so downstream consumers can drop the pnpm patch pinned against 0.1.11

ESM vs Metro Module Lowering

graph TD
    A[schema.ts source] --> B{Bundler}
    B -->|Node / esbuild / Rollup / Webpack 5| C[ESM link phase]
    B -->|Metro Babel transform| D[CJS-style factory]
    C --> E[Resolve all import / export bindings before any code runs]
    E --> F[Top-level statements execute in source order]
    F --> G[D.object pdef resolves correctly]
    D --> H[Hoist export object to top of factory]
    H --> I[Materialize first namespace import: decoder]
    I --> J[Second alias D for same module never assigned]
    J --> K[D undefined at call time]
    K --> L[TypeError: Cannot read property 'object' of undefined]
    style G fill:#163,color:#fff
    style L fill:#a22,color:#fff
Loading

Changed Files

File Change Type Summary
core/package.json Modified Version bump 0.1.140.1.15
core/src/json/schema.ts Modified Move exports to bottom, remove D / E aliases, inline shadowing bindings in object, pair, triple
core/src/json/decoder.ts Modified Move exports to bottom
core/src/json/encoder.ts Modified Move exports to bottom
core/src/future.ts Modified Move exports to bottom
core/src/list.ts Modified Move exports to bottom
core/src/maybe.ts Modified Move exports to bottom
core/src/remote-data.ts Modified Move exports to bottom
core/src/result.ts Modified Move exports to bottom
core/src/router.ts Modified Move exports to bottom
core/src/time.ts Modified Move exports to bottom
core/src/tree-map.ts Modified Move exports to bottom
core/src/tree-set.ts Modified Move exports to bottom
core/tests/suites/json.ts Modified Move exports to bottom
core/tests/suites/list.ts Modified Move exports to bottom
core/tests/suites/time.ts Modified Move exports to bottom
core/tests/suites/tree-map.ts Modified Move exports to bottom
core/tests/suites/tree-set.ts Modified Move exports to bottom
tasks/src/definition.ts Modified Move exports to bottom
tasks/src/explorer.ts Modified Move exports to bottom
tasks/src/postgres.ts Modified Move exports to bottom
tasks/src/store.ts Modified Move exports to bottom
tasks/src/worker.ts Modified Move exports to bottom
tasks/tests/suites/postgres.ts Modified Move exports to bottom
tasks/tests/suites/tasks/explorer.ts Modified Move exports to bottom
tasks/tests/suites/tasks/helpers.ts Modified Move exports to bottom
tasks/tests/suites/tasks/index.ts Modified Move exports to bottom
tasks/tests/suites/tasks/store.ts Modified Move exports to bottom
tasks/tests/suites/tasks/taskM.ts Modified Move exports to bottom
tasks/tests/suites/tasks/worker.ts Modified Move exports to bottom
task-explorer/backend/src/lib/api-schemas.ts Modified Move exports to bottom
task-explorer/backend/src/lib/decoders.ts Modified Move exports to bottom
task-explorer/backend/tests/helpers.ts Modified Move exports to bottom
task-explorer/frontend/src/lib/api.ts Modified Move exports to bottom
task-explorer/frontend/src/lib/taskFormatUtils.ts Modified Move exports to bottom
task-explorer/frontend/src/lib/taskStatsUtils.ts Modified Move exports to bottom
task-explorer/frontend/src/lib/taskUtils.ts Modified Move exports to bottom
task-explorer/frontend/src/lib/useClickOutside.ts Modified Move exports to bottom
task-explorer/frontend/src/lib/useDebounce.ts Modified Move exports to bottom

Testing & Feedback

  • Run the core, tasks, and task-explorer test suites to confirm no regression from the namespace-alias rewrite in core/src/json/schema.ts
  • Build @ambarltd/core and install the 0.1.15 artifact into a Metro-bundled Expo / React Native app that imports @ambarltd/core/json/schema at module top level; confirm no Cannot read property 'object' of undefined at module init
  • Round-trip decode / encode on at least one object, pair, and triple schema to verify the inlined construction paths in schema.ts still behave identically

If you find any bugs or have recommendations for improvements, please open an issue and assign it to me.

- Relocate `export { ... }` blocks from the top to the bottom of files across `core`, `tasks`, and `task-explorer` packages for consistent module structure.
- Refactor `core/src/json/schema.ts` to use namespace imports (`decoder.X`, `encoder.X`) instead of `D`/`E` aliases and import `DecoderOptional`/`EncoderOptional` types directly.
- Simplify several `Schema` constructors in `schema.ts` by inlining intermediate `decoder`/`encoder` variables.
- Bump `@ambarltd/core` version to `0.1.15`.
@rafaeelricco
rafaeelricco requested a review from lazamar May 12, 2026 15:07
@rafaeelricco rafaeelricco self-assigned this May 12, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@rafaeelricco

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

- Add `"packageManager": "pnpm@10.33.4"` to root, frontend, and backend `package.json` files.
- Remove `corepack use pnpm@latest-10` invocations from all Dockerfile stages, letting corepack resolve the version from `packageManager`.

@lazamar lazamar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ok

@rafaeelricco
rafaeelricco merged commit fb55137 into main May 12, 2026
5 checks passed
@ctuncay

ctuncay commented May 14, 2026

Copy link
Copy Markdown
Contributor

I suspect relocating all of the exports in all of the packages were unnecessary. Just making remove D / E aliases, inline shadowing bindings in object, pair, triple change in schema.ts would have worked.

I had a similar problem when building task-explorer. Except the ESM build issue was caused by a third party dependency, and I didn't have the option to change the source code. Fixing duplicate imports & exports in schema.ts would have been enough.

My analysis

The PR correctly identifies the bug, but the fix is much broader than necessary. The crash has two specific causes, both isolated to schema.ts:

  1. Duplicate namespace imports — ./decoder is imported twice (import * as decoder and import * as D), same for ./encoder. Metro's CJS transform deduplicates them, leaving D and E as undefined.
  2. Local variable shadowing — Inside object(), pair(), and triple(), there are const decoder = D.object(...) locals that shadow the module-scope decoder namespace. This works with D/E alive, but is
    fragile.

The export { ... } block placement (top vs bottom) is not the actual trigger. In spec-compliant ESM, export declarations are hoisted and linked before execution regardless of source position. The crash
is specifically D.object where D is undefined — that's the duplicate-alias dedup, not export ordering.

Alternative (minimal) fixes

Option A — Just remove the duplicate aliases (smallest change, ~30 lines in 1 file)
Delete import * as D and import * as E, rewrite D./E. to decoder./encoder., inline the shadowed locals in object/pair/triple. This is exactly what the PR does to schema.ts — but without touching the other 37 files.

Option B — Use inline export on each declaration
Replace the aggregate export { ... } block with export class Schema, export function object, export const json, etc. This is the most conventional TypeScript pattern. Metro can't mis-order what's declared and exported in a single statement. Also eliminates the need for a separate export block to keep in sync.

Option C — Publish ESM-only (or fix the build)
If @ambarltd/core compiles to CJS before publishing via tsc, the emitted exports.X = ... assignments inherit the source ordering. You could emit ESM ("type": "module" + module: "nodenext") so Metro and Node both consume true ESM. This is a bigger change but solves the class of problem permanently.

Option D — Named imports instead of namespace imports
Replace import * as decoder from "./decoder" with import { object, pair, ... } from "./decoder". Named imports don't trigger Metro's namespace-dedup logic at all.

Bottom line

The real fix is Option A — it's exactly the schema.ts changes already in this PR. Moving exports to the bottom in the other 37 files is harmless but unnecessary; those files don't have duplicate namespace imports and wouldn't hit this Metro bug. It's a stylistic normalization, not a bug fix.

@ctuncay
ctuncay deleted the update-exports branch May 14, 2026 17:52
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.

3 participants