feat: add ponyfill-temporal package - #73
Conversation
Reviewer's GuideIntroduces a new Sequence diagram for loadTemporal ponyfill behaviorsequenceDiagram
actor App
participant ponyfill_temporal as ponyfill_temporal
participant globalThis as globalThis
participant js_temporal_polyfill as js_temporal_polyfill
App->>ponyfill_temporal: loadTemporal()
ponyfill_temporal->>globalThis: isNativeTemporalAvailable()
alt [native Temporal available]
ponyfill_temporal->>globalThis: read Temporal, Intl, Date.prototype.toTemporalInstant
ponyfill_temporal-->>App: TemporalApi (native)
else [native Temporal not available]
ponyfill_temporal->>js_temporal_polyfill: import("@js-temporal/polyfill")
js_temporal_polyfill-->>ponyfill_temporal: Temporal, Intl, toTemporalInstant
ponyfill_temporal-->>App: TemporalApi (polyfill)
end
Sequence diagram for installTemporal global polyfill behaviorsequenceDiagram
actor App
participant ponyfill_temporal as ponyfill_temporal
participant globalThis as globalThis
App->>ponyfill_temporal: installTemporal()
ponyfill_temporal->>ponyfill_temporal: loadTemporal()
ponyfill_temporal->>globalThis: isNativeTemporalAvailable()
alt [native Temporal available]
ponyfill_temporal-->>App: TemporalApi (native, no mutation)
else [native Temporal not available]
ponyfill_temporal->>globalThis: set Temporal = api.Temporal
ponyfill_temporal->>globalThis: set Date.prototype.toTemporalInstant = api.toTemporalInstant
ponyfill_temporal-->>App: TemporalApi (polyfill installed globally)
end
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Warning Review limit reached
Next review available in: 52 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (12)
WalkthroughAdds the ChangesTemporal ponyfill
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant ConsumerPackage
participant PonyfillTemporal
participant globalThis
participant temporalPolyfill
ConsumerPackage->>PonyfillTemporal: import Temporal API
PonyfillTemporal->>globalThis: check native Temporal
alt native Temporal exists
globalThis-->>PonyfillTemporal: provide native Temporal surface
else native Temporal absent
PonyfillTemporal->>temporalPolyfill: dynamically import fallback
temporalPolyfill-->>PonyfillTemporal: provide polyfill surface
end
PonyfillTemporal-->>ConsumerPackage: expose Temporal, Intl, and helpers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Hey - I've left some high level feedback:
- In
loadTemporal, the native path assumesDate.prototype.toTemporalInstantexists and is compatible with the polyfill’s type; consider defensively handling runtimes that shipTemporalbut nottoTemporalInstantor where its signature diverges. - The
TemporalGlobal/MutableTemporalGlobalinterfaces modelIntlandDateas always present onglobalThis; if this package is intended to be used in non-browser/non-Node environments, it may be worth narrowing these typings or guarding property access to avoid unsafe assumptions.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `loadTemporal`, the native path assumes `Date.prototype.toTemporalInstant` exists and is compatible with the polyfill’s type; consider defensively handling runtimes that ship `Temporal` but not `toTemporalInstant` or where its signature diverges.
- The `TemporalGlobal`/`MutableTemporalGlobal` interfaces model `Intl` and `Date` as always present on `globalThis`; if this package is intended to be used in non-browser/non-Node environments, it may be worth narrowing these typings or guarding property access to avoid unsafe assumptions.Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/ponyfill-temporal/src/install-temporal.test.ts`:
- Around line 5-33: Update the installTemporal test setup to snapshot the
Date.prototype.toTemporalInstant property descriptor and restore it in cleanup
alongside globalRef.Temporal. In the native-absent test, assert that
Date.prototype.toTemporalInstant equals api.toTemporalInstant after
installTemporal completes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 9adbfec4-f3e7-4ae7-a0e4-cf89ab342e57
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (11)
.changeset/ponyfill-temporal.mdpackages/ponyfill-temporal/.gitignorepackages/ponyfill-temporal/README.mdpackages/ponyfill-temporal/eslint.config.jspackages/ponyfill-temporal/package.jsonpackages/ponyfill-temporal/src/index.tspackages/ponyfill-temporal/src/install-temporal.test.tspackages/ponyfill-temporal/src/install-temporal.tspackages/ponyfill-temporal/src/load-temporal.test.tspackages/ponyfill-temporal/src/load-temporal.tspackages/ponyfill-temporal/tsconfig.json
| const globalRef = globalThis as { Temporal?: unknown }; | ||
| const originalTemporal = globalRef.Temporal; | ||
|
|
||
| function restoreTemporal(): void { | ||
| if (originalTemporal === undefined) { | ||
| delete globalRef.Temporal; | ||
| } else { | ||
| globalRef.Temporal = originalTemporal; | ||
| } | ||
| } | ||
|
|
||
| afterEach(() => { | ||
| restoreTemporal(); | ||
| }); | ||
|
|
||
| describe("installTemporal", () => { | ||
| test("installs the polyfill onto globalThis when native is absent", async () => { | ||
| delete globalRef.Temporal; | ||
| expect(isNativeTemporalAvailable()).toBe(false); | ||
|
|
||
| const api = await installTemporal(); | ||
|
|
||
| // Temporal is now globally available. | ||
| expect(isNativeTemporalAvailable()).toBe(true); | ||
| expect(globalRef.Temporal).toBe(api.Temporal); | ||
|
|
||
| const date = api.Temporal.PlainDate.from("2021-06-30"); | ||
| expect(date.year).toBe(2021); | ||
| }); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Restore and verify Date.prototype.toTemporalInstant.
The polyfill-install test leaks this prototype mutation into later tests, while never asserting the documented installation behavior. Snapshot and restore its property descriptor alongside Temporal, then assert it equals api.toTemporalInstant.
Proposed fix
const globalRef = globalThis as { Temporal?: unknown };
const originalTemporal = globalRef.Temporal;
+const originalToTemporalInstant = Object.getOwnPropertyDescriptor(
+ Date.prototype,
+ "toTemporalInstant",
+);
function restoreTemporal(): void {
if (originalTemporal === undefined) {
delete globalRef.Temporal;
} else {
globalRef.Temporal = originalTemporal;
}
+
+ if (originalToTemporalInstant === undefined) {
+ delete (Date.prototype as { toTemporalInstant?: unknown }).toTemporalInstant;
+ } else {
+ Object.defineProperty(
+ Date.prototype,
+ "toTemporalInstant",
+ originalToTemporalInstant,
+ );
+ }
}
@@
expect(isNativeTemporalAvailable()).toBe(true);
expect(globalRef.Temporal).toBe(api.Temporal);
+ expect(
+ (Date.prototype as { toTemporalInstant?: unknown }).toTemporalInstant,
+ ).toBe(api.toTemporalInstant);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const globalRef = globalThis as { Temporal?: unknown }; | |
| const originalTemporal = globalRef.Temporal; | |
| function restoreTemporal(): void { | |
| if (originalTemporal === undefined) { | |
| delete globalRef.Temporal; | |
| } else { | |
| globalRef.Temporal = originalTemporal; | |
| } | |
| } | |
| afterEach(() => { | |
| restoreTemporal(); | |
| }); | |
| describe("installTemporal", () => { | |
| test("installs the polyfill onto globalThis when native is absent", async () => { | |
| delete globalRef.Temporal; | |
| expect(isNativeTemporalAvailable()).toBe(false); | |
| const api = await installTemporal(); | |
| // Temporal is now globally available. | |
| expect(isNativeTemporalAvailable()).toBe(true); | |
| expect(globalRef.Temporal).toBe(api.Temporal); | |
| const date = api.Temporal.PlainDate.from("2021-06-30"); | |
| expect(date.year).toBe(2021); | |
| }); | |
| const globalRef = globalThis as { Temporal?: unknown }; | |
| const originalTemporal = globalRef.Temporal; | |
| const originalToTemporalInstant = Object.getOwnPropertyDescriptor( | |
| Date.prototype, | |
| "toTemporalInstant", | |
| ); | |
| function restoreTemporal(): void { | |
| if (originalTemporal === undefined) { | |
| delete globalRef.Temporal; | |
| } else { | |
| globalRef.Temporal = originalTemporal; | |
| } | |
| if (originalToTemporalInstant === undefined) { | |
| delete (Date.prototype as { toTemporalInstant?: unknown }).toTemporalInstant; | |
| } else { | |
| Object.defineProperty( | |
| Date.prototype, | |
| "toTemporalInstant", | |
| originalToTemporalInstant, | |
| ); | |
| } | |
| } | |
| afterEach(() => { | |
| restoreTemporal(); | |
| }); | |
| describe("installTemporal", () => { | |
| test("installs the polyfill onto globalThis when native is absent", async () => { | |
| delete globalRef.Temporal; | |
| expect(isNativeTemporalAvailable()).toBe(false); | |
| const api = await installTemporal(); | |
| // Temporal is now globally available. | |
| expect(isNativeTemporalAvailable()).toBe(true); | |
| expect(globalRef.Temporal).toBe(api.Temporal); | |
| expect( | |
| (Date.prototype as { toTemporalInstant?: unknown }).toTemporalInstant, | |
| ).toBe(api.toTemporalInstant); | |
| const date = api.Temporal.PlainDate.from("2021-06-30"); | |
| expect(date.year).toBe(2021); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ponyfill-temporal/src/install-temporal.test.ts` around lines 5 - 33,
Update the installTemporal test setup to snapshot the
Date.prototype.toTemporalInstant property descriptor and restore it in cleanup
alongside globalRef.Temporal. In the native-absent test, assert that
Date.prototype.toTemporalInstant equals api.toTemporalInstant after
installTemporal completes.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/ponyfill-temporal/src/temporal.ts (1)
23-28: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winPreserve or document the complete public type surface.
The implementation explicitly omits types such as
Temporal.RoundingOptions<Units>, so this is not a drop-in type replacement for consumers using those public names. Re-export the completetemporal-specsurface or document and test the intentional incompatibility.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ponyfill-temporal/src/temporal.ts` around lines 23 - 28, Update the generated ambient type bindings near the Temporal/Intl namespace merge to preserve the complete public temporal-spec surface, including generic helper types such as Temporal.RoundingOptions<Units>; alternatively, explicitly document and add tests for the intentional incompatibility if those types must remain omitted.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/ponyfill-temporal/src/temporal.ts`:
- Around line 23-28: Update the generated ambient type bindings near the
Temporal/Intl namespace merge to preserve the complete public temporal-spec
surface, including generic helper types such as Temporal.RoundingOptions<Units>;
alternatively, explicitly document and add tests for the intentional
incompatibility if those types must remain omitted.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 74f318a3-0d44-44d6-9052-3380bfa4613d
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (39)
.changeset/consume-ponyfill-temporal.md.changeset/ponyfill-temporal.mdpackages/format-temporal/package.jsonpackages/format-temporal/src/format-temporal.test.tspackages/format-temporal/src/format-temporal.tspackages/interval-temporal/package.jsonpackages/interval-temporal/src/are-intervals-overlapping.test.tspackages/interval-temporal/src/comparators.test.tspackages/interval-temporal/src/comparators.tspackages/interval-temporal/src/normalize-intervals.test.tspackages/parse-temporal/package.jsonpackages/parse-temporal/src/parse-plain-date-time-yyyymmddhhmmp.test.tspackages/parse-temporal/src/parse-plain-date-time-yyyymmddhhmmp.tspackages/parse-temporal/src/parse-plain-date.test.tspackages/parse-temporal/src/parse-plain-date.tspackages/parse-temporal/src/parse-plain-time-hhmm.test.tspackages/parse-temporal/src/parse-plain-time-hhmm.tspackages/ponyfill-temporal/src/index.tspackages/ponyfill-temporal/src/load-temporal.tspackages/ponyfill-temporal/src/temporal.tspackages/superjson-temporal/package.jsonpackages/superjson-temporal/src/register-super-json-temporal.test.tspackages/superjson-temporal/src/register-super-json-temporal.tspackages/temporal-quarter-fns/package.jsonpackages/temporal-quarter-fns/src/get-quarter.test.tspackages/temporal-quarter-fns/src/get-quarter.tspackages/temporal-quarter-fns/src/temporal-with-year-month.tspackages/temporal-zod/package.jsonpackages/temporal-zod/src/base/duration.tspackages/temporal-zod/src/base/index.test.tspackages/temporal-zod/src/base/instant.tspackages/temporal-zod/src/base/plain-date-time.tspackages/temporal-zod/src/base/plain-date.tspackages/temporal-zod/src/base/plain-month-day.tspackages/temporal-zod/src/base/plain-time.tspackages/temporal-zod/src/base/plain-year-month.tspackages/temporal-zod/src/base/zoned-date-time.tspackages/temporal-zod/src/json-schema.test.tspackages/temporal-zod/src/json-schemas.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- .changeset/ponyfill-temporal.md
- packages/ponyfill-temporal/src/index.ts
- packages/ponyfill-temporal/src/load-temporal.ts
Adopt the oxlint + oxfmt toolchain (replacing Biome/ESLint) from master. - Migrate the new ponyfill-temporal package off ESLint (remove eslint.config.js, eslint deps, per-package lint script). - Keep this branch's ponyfill-temporal consumption: packages depend on ponyfill-temporal instead of temporal-polyfill/temporal-spec directly. - Fix an oxlint finding in load-temporal.ts (import() type annotation -> import type). - Port the full @macalinao/biome-config/base rule set into .oxlintrc.json so the previous lint surface is preserved under oxlint.
temporal-polyfill 0.3 rendered `{ year: "numeric", month: "long" }` for
en-US as "2023 May"; 1.x produces the correct "May 2023".
Master merged #73 (ponyfill-temporal), which swaps temporal-zod's Temporal import to the ponyfill and drops the direct temporal-polyfill dependency. - Resolve import conflicts in the six base files: keep master's ponyfill-temporal import alongside this branch's iso-pattern-parts imports; migrate orpc.test.ts off temporal-polyfill too. - Preload temporal-polyfill/full/global in bun tests (bunfig.toml): Bun has no native Temporal and the ponyfill's default fallback omits non-ISO calendar data, which the calendar-annotation tests exercise. - Satisfy oxlint x isolatedDeclarations on the exported patterns: template-literal exports need an explicit type (TS9010), so widen with 'as string'; where the interpolated parts are already string-typed, oxlint flags the assertion as unnecessary, so those sites carry a disable comment. - Regenerate bun.lock via bun install.
Summary
Adds
ponyfill-temporal— a ponyfill for the TC39 Temporal API — and migrates the entire monorepo to consume Temporal through it.ponyfill-temporalis now the sole owner of thetemporal-polyfill/temporal-specdependencies.ponyfill-temporalglobalThis.Temporalwhen present; otherwise conditionally loadstemporal-polyfillvia a lazy dynamicimport()(never a static import, so native runtimes never load the polyfill chunk).Temporal/Intl/toTemporalInstant, resolved once via top-level await (const api = await loadTemporal()intemporal.ts). Consumers use them synchronously —new Temporal.PlainDate(...),Temporal.Now.instant()— with noawaitat the call site.temporal-spectype namespaces (a valueconstmerged with adeclare namespace), soTemporal.PlainDateworks in value and type positions andimport type { Temporal } from "ponyfill-temporal"works — mirroring howtemporal-polyfillexports them. The type namespaces are generated fromtemporal-spec's public surface (generic option helper types are omitted).loadTemporal(),installTemporal()(opt-in global install),isNativeTemporalAvailable(), plus typesTemporalApi/TemporalIntl/TemporalSpecModule.Dependency stack (unchanged rationale)
temporal-spec@^1.0.0— runtime-free, models native Temporal, shipsindex.d.ts+global.d.ts.temporal-polyfill@^1.0.1— lightweight, active; typed viatemporal-spec.Repo migration (all packages)
parse-temporal,format-temporal,interval-temporal,temporal-quarter-fns,superjson-temporal,temporal-zod:import { Temporal } from "temporal-polyfill"and type importsimport type { Temporal } from "temporal-spec"→... from "ponyfill-temporal"(28 source + test files). Synchronous usages are unchanged.package.json: removed directtemporal-polyfill/temporal-spec(dependencies, devDependencies, and peerDependencies); added"ponyfill-temporal": "workspace:*"as a dependency. Onlyponyfill-temporalnow depends ontemporal-polyfill/temporal-spec.Two adjustments required by the
temporal-spec0.3 → 1.0 bump (flagged, not forced)interval-temporal/comparators.ts:Temporal.X.compareis now typednumber(temporal-spec droppedComparisonResult); narrowed the 5 comparator returns to-1 | 0 | 1(runtime behavior unchanged).format-temporal.ts:Intl.FormattableTemporalObjectnow excludesZonedDateTime; thedefaultswitch branch (which only receives directly-formattable types) asserts toIntl.FormattableTemporalObject. Also,ponyfill-temporal'sIntlvalue is typed as the Temporal-awareDateTimeFormat(mirrored fromtemporal-spec, since it declaresIntltype-only) sonew Intl.DateTimeFormat(...)is assignable to format-temporal's parameter.Verification (CI-relevant)
bun install— workspace dep wired, lockfile updated.bun run build— all 7 packages build, emitdist/, satisfyisolatedDeclarations; TLA emits natively under NodeNext/ES2024.bun run lint— exit 0 (biome + turbo eslint).format-temporalICU failure ("2023 May"vs"May 2023"forPlainYearMonth), which is a locale-data quirk unrelated to this change. Counts: ponyfill 5, parse 11, superjson 1, interval 11, quarter-fns 3, format 6/7 (1 known ICU fail), temporal-zod 103.dist/contains no staticfrom "temporal-polyfill"— onlyawait import("temporal-polyfill")insideloadTemporal's fallback branch — so a native-Temporal runtime never loads the polyfill.Changesets
ponyfill-temporal— minor (updated to describe the sync TLA exports).parse-temporal,format-temporal,interval-temporal,temporal-quarter-fns,superjson-temporal,temporal-zod— minor (consume Temporal viaponyfill-temporal; direct temporal-polyfill/temporal-spec deps removed).npm publish + trusted publishing
ponyfill-temporal@0.0.1is already published (reserves the name); not republished for this change — future versions publish via CI.Pending user action (2FA-gated, could not run from this environment —
npm trustneeds an interactive OTP session; token returned 403):Summary by CodeRabbit
ponyfill-temporal, a TC39 Temporal ponyfill that prefers nativeglobalThis.Temporaland conditionally loads a polyfill when missing.loadTemporal()(non-global access),isNativeTemporalAvailable()(capability detection), andinstallTemporal()(opt-in global install).Temporalviaponyfill-temporalinstead of direct polyfill/spec usage.