diff --git a/README.md b/README.md index 37da859..f55b9d6 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,66 @@ # @agape/temporal -A stand-in for Temporal that lets you program for systems with or without Temporal support. +Access the Temporal namespace safely, even in environments where it's not natively available. -## ✨ Functions +@agape/temporal provides a drop-in mechanism for working with Temporal objects +without requiring the native Temporal API to be installed. If Temporal is +unavailable, this library provides stub implementations that raise clear runtime +errors, allowing your code to fail gracefully or fall back to alternatives. -### `hasTemporal(): boolean` -Check if Temporal is available in the current environment. - -### `getTemporal(): TemporalLike` -Get the Temporal object. Returns real Temporal if available, typed stub if not. - -### `TemporalLike` -Type alias for the complete Temporal namespace. Use for type annotations. - ---- - -## 🚀 Example +## 🚀 Get Started ```typescript -import { getTemporal, hasTemporal } from '@agape/temporal'; +import { Temporal, hasTemporal, setTemporal } from '@agape/temporal'; +// Always works - checks for Temporal availability if (hasTemporal()) { - const Temporal = getTemporal(); const now = Temporal.PlainDateTime.from('2025-09-19T10:00'); - console.log(now.toString()); + console.log(now.toString()); // "2025-09-19T10:00:00" } else { - console.warn('Temporal not available, using Date fallback'); - const fallback = new Date(); + console.log('Temporal not available, using fallback'); } ``` -## Environment Support +### With Polyfill + +If the native Temporal exists, or polyfill has been set on the `globalThis`, that +will be the implementation used. + +```typescript +import { Temporal as TemporalPolyfill } from '@js-temporal/polyfill'; +(globalThis as any)['Temporal'] = TemporalPolyfill; + +import { Temporal } from '@agape/temporal'; + +// Temporal just works +const date = Temporal.PlainDate.from('2025-09-19'); +const duration = Temporal.Duration.from('PT1H30M'); +``` + +### With Agape Configuration + +You can configure which implementation Agape uses. + +```typescript +import { Temporal as TemporalPolyfill } from '@js-temporal/polyfill'; +import { setTemporal, Temporal } from '@agape/temporal'; + +setTemporal(TemporalPolyfill); + +const date = Temporal.PlainDate.from('2025-09-19'); +const duration = Temporal.Duration.from('PT1H30M'); +``` + +## 📖 API + +### `Temporal` Namespace +Use the full Temporal API - `Temporal.PlainDateTime`, `Temporal.PlainDate`, `Temporal.Duration`, etc. Works with real Temporal or provides helpful error stubs. + +### `hasTemporal(): boolean` +Check if Temporal is available before using it. -- **Native Temporal**: Node.js 20+, Chrome 84+, Firefox 95+, Safari 15.4+ -- **Polyfill**: Works with any configured polyfill -- **Fallback**: Typed stub with helpful error messages +### `setTemporal(temporal): void` +Configure your preferred Temporal implementation (polyfill, custom, etc.). --- diff --git a/package.json b/package.json index f323465..52e8882 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@agape/temporal", - "version": "0.1.0", - "description": "Typed, dependency-free access to Temporal with fallback stub", + "version": "0.2.0", + "description": "Temporal namespace", "main": "./cjs/index.js", "module": "./es2020/index.js", "author": { diff --git a/src/index.spec.ts b/src/index.spec.ts index e5f7c21..cce875e 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -4,12 +4,12 @@ describe('@agape/temporal', () => { // Store original globalThis.Temporal const originalTemporal = (globalThis as any).Temporal; - beforeEach(() => { + beforeEach(async () => { // Reset global state delete (globalThis as any).Temporal; }); - afterEach(() => { + afterEach(async () => { // Restore original Temporal if it existed if (originalTemporal) { (globalThis as any).Temporal = originalTemporal; @@ -23,7 +23,7 @@ describe('@agape/temporal', () => { // Clear module cache and re-import jest.resetModules(); const { hasTemporal } = await import('./index'); - + const mockTemporal = { PlainDateTime: { from: jest.fn(), @@ -38,7 +38,7 @@ describe('@agape/temporal', () => { // Clear module cache and re-import jest.resetModules(); const { hasTemporal } = await import('./index'); - + const mockPolyfill = { PlainDateTime: { from: jest.fn(), @@ -53,7 +53,7 @@ describe('@agape/temporal', () => { // Clear module cache and re-import jest.resetModules(); const { hasTemporal } = await import('./index'); - + expect(hasTemporal()).toBe(false); }); @@ -61,7 +61,7 @@ describe('@agape/temporal', () => { // Clear module cache and re-import jest.resetModules(); const { hasTemporal } = await import('./index'); - + const mockTemporal = { PlainDateTime: { from: jest.fn(), @@ -71,182 +71,269 @@ describe('@agape/temporal', () => { // First call should check and cache expect(hasTemporal()).toBe(true); - + // Second call should use cached result expect(hasTemporal()).toBe(true); }); }); - describe('getTemporal', () => { - it('should return native Temporal when available', async () => { + describe('setTemporal', () => { + it('should set agape temporal and allow creating real temporal objects', async () => { // Clear module cache and re-import jest.resetModules(); - const { getTemporal } = await import('./index'); + // Create a mock temporal polyfill const mockTemporal = { PlainDateTime: { - from: jest.fn().mockReturnValue({ - toString: jest.fn().mockReturnValue('2025-09-19T10:00:00'), - }), + from: jest.fn().mockImplementation((value: string) => ({ + toString: () => `2025-09-19T10:00:00`, + year: 2025, + month: 9, + day: 19, + hour: 10, + minute: 0, + second: 0, + })), + }, + PlainDate: { + from: jest.fn().mockImplementation((value: string) => ({ + toString: () => `2025-09-19`, + year: 2025, + month: 9, + day: 19, + })), + }, + Duration: { + from: jest.fn().mockImplementation((value: string) => ({ + toString: () => `PT1H30M`, + hours: 1, + minutes: 30, + })), }, }; - (globalThis as any).Temporal = mockTemporal; - const temporal = getTemporal(); - expect(temporal).toBe(mockTemporal); - expect(temporal.PlainDateTime.from).toBeDefined(); + const { setTemporal, Temporal, hasTemporal } = await import('./index'); + setTemporal(mockTemporal as any); + + expect(hasTemporal()).toBe(true); + + // Test creating real temporal objects using the namespace + const dateTime = Temporal.PlainDateTime.from('2025-09-19T10:00'); + expect(dateTime.toString()).toBe('2025-09-19T10:00:00'); + expect(dateTime.year).toBe(2025); + expect(dateTime.month).toBe(9); + expect(dateTime.day).toBe(19); + + const date = Temporal.PlainDate.from('2025-09-19'); + expect(date.toString()).toBe('2025-09-19'); + expect(date.year).toBe(2025); + + const duration = Temporal.Duration.from('PT1H30M'); + expect(duration.toString()).toBe('PT1H30M'); + expect(duration.hours).toBe(1); + expect(duration.minutes).toBe(30); }); - it('should return polyfill Temporal when available on globalThis', async () => { + it('should prioritize agape temporal over globalThis', async () => { // Clear module cache and re-import jest.resetModules(); - const { getTemporal } = await import('./index'); - const mockPolyfill = { + const agapeTemporal = { PlainDateTime: { - from: jest.fn().mockReturnValue({ - toString: jest.fn().mockReturnValue('2025-09-19T10:00:00'), - }), + from: jest.fn().mockImplementation(() => ({ + toString: () => 'agape-temporal-result', + source: 'agape', + })), }, }; - (globalThis as any).Temporal = mockPolyfill; - const temporal = getTemporal(); - expect(temporal).toBe(mockPolyfill); - expect(temporal.PlainDateTime.from).toBeDefined(); - }); - - it('should return cached Temporal on subsequent calls', async () => { - // Clear module cache and re-import - jest.resetModules(); - const { getTemporal } = await import('./index'); - - const mockTemporal = { + const globalTemporal = { PlainDateTime: { - from: jest.fn(), + from: jest.fn().mockImplementation(() => ({ + toString: () => 'global-temporal-result', + source: 'global', + })), }, }; - (globalThis as any).Temporal = mockTemporal; - const temporal1 = getTemporal(); - const temporal2 = getTemporal(); + (globalThis as any).Temporal = globalTemporal; + + const { setTemporal, Temporal, hasTemporal } = await import('./index'); + setTemporal(agapeTemporal as any); - expect(temporal1).toBe(temporal2); - expect(temporal1).toBe(mockTemporal); + expect(hasTemporal()).toBe(true); + + // Should use agape temporal, not global + const result = Temporal.PlainDateTime.from('2025-09-19T10:00'); + expect(result.toString()).toBe('agape-temporal-result'); + expect((result as any).source).toBe('agape'); }); - it('should return typed stub when Temporal is not available', async () => { + it('should fall back to globalThis when agape temporal is cleared', async () => { // Clear module cache and re-import jest.resetModules(); - const { getTemporal } = await import('./index'); - const temporal = getTemporal(); - expect(temporal).toBeDefined(); - expect(typeof temporal).toBe('object'); - }); + const agapeTemporal = { + PlainDateTime: { + from: jest.fn().mockImplementation(() => ({ + toString: () => 'agape-temporal-result', + source: 'agape', + })), + }, + }; - it('should throw descriptive error when accessing stub properties', async () => { - // Clear module cache and re-import - jest.resetModules(); - const { getTemporal } = await import('./index'); + const globalTemporal = { + PlainDateTime: { + from: jest.fn().mockImplementation(() => ({ + toString: () => 'global-temporal-result', + source: 'global', + })), + }, + }; + + (globalThis as any).Temporal = globalTemporal; - const temporal = getTemporal(); + const { setTemporal, Temporal, hasTemporal } = await import('./index'); + setTemporal(agapeTemporal as any); - expect(() => { - (temporal as any).PlainDateTime; - }).toThrow('Temporal is not available (accessed property: PlainDateTime). Install @js-temporal/polyfill or use Node 20+/modern browsers.'); - }); - - it('should throw descriptive error when accessing nested stub properties', async () => { - // Clear module cache and re-import - jest.resetModules(); - const { getTemporal } = await import('./index'); + // First should use agape + let result = Temporal.PlainDateTime.from('2025-09-19T10:00'); + expect(result.toString()).toBe('agape-temporal-result'); - const temporal = getTemporal(); + // Clear agape temporal by setting it to undefined + setTemporal(undefined as any); - expect(() => { - (temporal as any).PlainDateTime.from; - }).toThrow('Temporal is not available (accessed property: PlainDateTime). Install @js-temporal/polyfill or use Node 20+/modern browsers.'); + expect(hasTemporal()).toBe(true); + + // Now should use global + result = Temporal.PlainDateTime.from('2025-09-19T10:00'); + expect(result.toString()).toBe('global-temporal-result'); + expect((result as any).source).toBe('global'); }); - it('should work with different property names', async () => { + it('should use stubs when no temporal is available', async () => { // Clear module cache and re-import jest.resetModules(); - const { getTemporal } = await import('./index'); - - const temporal = getTemporal(); + const { Temporal, hasTemporal } = await import('./index'); + + expect(hasTemporal()).toBe(false); + expect(Temporal.PlainDateTime).toBeDefined(); + expect(typeof Temporal.PlainDateTime).toBe('function'); + // Should throw error when trying to create objects expect(() => { - (temporal as any).Now; - }).toThrow('Temporal is not available (accessed property: Now). Install @js-temporal/polyfill or use Node 20+/modern browsers.'); + Temporal.PlainDateTime.from('2025-09-19T10:00'); + }).toThrow('Temporal required. Use a JavaScript runtime which has Temporal or use a polyfill such as @js-temporal/polyfill or temporal-polyfill.'); }); }); - describe('TemporalLike type', () => { - it('should be compatible with native Temporal', async () => { + describe('Temporal namespace with globalThis polyfill', () => { + it('should work with native Temporal on globalThis', async () => { // Clear module cache and re-import jest.resetModules(); - const { getTemporal } = await import('./index'); + // Set up a mock temporal polyfill on globalThis const mockTemporal = { PlainDateTime: { - from: jest.fn(), + from: jest.fn().mockImplementation((value: string) => ({ + toString: () => `2025-09-19T10:00:00`, + year: 2025, + month: 9, + day: 19, + hour: 10, + minute: 0, + second: 0, + })), }, PlainDate: { - from: jest.fn(), - }, - PlainTime: { - from: jest.fn(), + from: jest.fn().mockImplementation((value: string) => ({ + toString: () => `2025-09-19`, + year: 2025, + month: 9, + day: 19, + })), }, - Now: { - plainDateTime: jest.fn(), + Duration: { + from: jest.fn().mockImplementation((value: string) => ({ + toString: () => `PT2H45M`, + hours: 2, + minutes: 45, + })), }, }; (globalThis as any).Temporal = mockTemporal; - const temporal = getTemporal(); - expect(temporal).toBeDefined(); - expect(temporal.PlainDateTime).toBeDefined(); - expect(temporal.PlainDate).toBeDefined(); - expect(temporal.PlainTime).toBeDefined(); - expect(temporal.Now).toBeDefined(); + const { Temporal, hasTemporal } = await import('./index'); + + expect(hasTemporal()).toBe(true); + + // Test creating real temporal objects using the namespace + const dateTime = Temporal.PlainDateTime.from('2025-09-19T10:00'); + expect(dateTime.toString()).toBe('2025-09-19T10:00:00'); + expect(dateTime.year).toBe(2025); + expect(dateTime.month).toBe(9); + expect(dateTime.day).toBe(19); + + const date = Temporal.PlainDate.from('2025-09-19'); + expect(date.toString()).toBe('2025-09-19'); + expect(date.year).toBe(2025); + + const duration = Temporal.Duration.from('PT2H45M'); + expect(duration.toString()).toBe('PT2H45M'); + expect(duration.hours).toBe(2); + expect(duration.minutes).toBe(45); }); - it('should be compatible with polyfill Temporal', async () => { + it('should work with polyfill Temporal on globalThis', async () => { // Clear module cache and re-import jest.resetModules(); - const { getTemporal } = await import('./index'); + // Set up a mock polyfill on globalThis const mockPolyfill = { PlainDateTime: { - from: jest.fn(), + from: jest.fn().mockImplementation((value: string) => ({ + toString: () => `polyfill-${value}`, + source: 'polyfill', + })), }, PlainDate: { - from: jest.fn(), + from: jest.fn().mockImplementation((value: string) => ({ + toString: () => `polyfill-date-${value}`, + source: 'polyfill', + })), }, - PlainTime: { - from: jest.fn(), - }, - Now: { - plainDateTime: jest.fn(), + Duration: { + from: jest.fn().mockImplementation((value: string) => ({ + toString: () => `polyfill-duration-${value}`, + source: 'polyfill', + })), }, }; (globalThis as any).Temporal = mockPolyfill; - const temporal = getTemporal(); - expect(temporal).toBeDefined(); - expect(temporal.PlainDateTime).toBeDefined(); - expect(temporal.PlainDate).toBeDefined(); - expect(temporal.PlainTime).toBeDefined(); - expect(temporal.Now).toBeDefined(); + const { Temporal, hasTemporal } = await import('./index'); + + expect(hasTemporal()).toBe(true); + + // Test creating real temporal objects using the namespace + const dateTime = Temporal.PlainDateTime.from('2025-09-19T10:00'); + expect(dateTime.toString()).toBe('polyfill-2025-09-19T10:00'); + expect((dateTime as any).source).toBe('polyfill'); + + const date = Temporal.PlainDate.from('2025-09-19'); + expect(date.toString()).toBe('polyfill-date-2025-09-19'); + expect((date as any).source).toBe('polyfill'); + + const duration = Temporal.Duration.from('PT1H30M'); + expect(duration.toString()).toBe('polyfill-duration-PT1H30M'); + expect((duration as any).source).toBe('polyfill'); }); }); - + describe('integration scenarios', () => { it('should handle the example usage from documentation', async () => { // Clear module cache and re-import jest.resetModules(); - const { getTemporal, hasTemporal } = await import('./index'); const mockTemporal = { PlainDateTime: { @@ -257,8 +344,9 @@ describe('@agape/temporal', () => { }; (globalThis as any).Temporal = mockTemporal; + const { Temporal, hasTemporal } = await import('./index'); + if (hasTemporal()) { - const Temporal = getTemporal(); const now = Temporal.PlainDateTime.from('2025-09-19T10:00'); expect(now.toString()).toBe('2025-09-19T10:00:00'); } else { @@ -270,7 +358,7 @@ describe('@agape/temporal', () => { // Clear module cache and re-import jest.resetModules(); const { hasTemporal } = await import('./index'); - + if (hasTemporal()) { throw new Error('Expected Temporal to not be available'); } else { @@ -284,8 +372,8 @@ describe('@agape/temporal', () => { it('should work in decorator context (synchronous)', async () => { // Clear module cache and re-import jest.resetModules(); - const { getTemporal } = await import('./index'); - + const { Temporal } = await import('./index'); + const mockTemporal = { PlainDateTime: { from: jest.fn(), @@ -295,41 +383,38 @@ describe('@agape/temporal', () => { // Simulate decorator usage - must be synchronous function temporalDecorator() { - const temporal = getTemporal(); - return function(target: any, propertyKey: string) { + return function() { // Decorator logic here - expect(temporal).toBeDefined(); + expect(Temporal).toBeDefined(); }; } - class TestClass { - @temporalDecorator() - testProperty: string = 'test'; - } - - expect(new TestClass()).toBeDefined(); + // Test that the decorator function works + const decorator = temporalDecorator(); + expect(typeof decorator).toBe('function'); + + // Test that Temporal is available synchronously + expect(Temporal).toBeDefined(); }); }); describe('error handling', () => { - it('should handle stub access with different property names', async () => { + it('should throw error when trying to instantiate stub classes', async () => { // Clear module cache and re-import jest.resetModules(); - const { getTemporal } = await import('./index'); - - const temporal = getTemporal(); - + const { Temporal } = await import('./index'); + expect(() => { - (temporal as any).PlainDateTime; - }).toThrow('Temporal is not available (accessed property: PlainDateTime). Install @js-temporal/polyfill or use Node 20+/modern browsers.'); - + new (Temporal as any).PlainDateTime(); + }).toThrow('Temporal required. Use a JavaScript runtime which has Temporal or use a polyfill such as @js-temporal/polyfill or temporal-polyfill.'); + expect(() => { - (temporal as any).Now; - }).toThrow('Temporal is not available (accessed property: Now). Install @js-temporal/polyfill or use Node 20+/modern browsers.'); - + new (Temporal as any).Duration(); + }).toThrow('Temporal required. Use a JavaScript runtime which has Temporal or use a polyfill such as @js-temporal/polyfill or temporal-polyfill.'); + expect(() => { - (temporal as any).Duration; - }).toThrow('Temporal is not available (accessed property: Duration). Install @js-temporal/polyfill or use Node 20+/modern browsers.'); + new (Temporal as any).TimeZone(); + }).toThrow('Temporal required. Use a JavaScript runtime which has Temporal or use a polyfill such as @js-temporal/polyfill or temporal-polyfill.'); }); }); -}); \ No newline at end of file +}); diff --git a/src/index.ts b/src/index.ts index ca6520d..998f4ae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,110 +1,366 @@ -import type { Temporal } from "@js-temporal/polyfill"; +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { Temporal as TemporalPolyfill } from "@js-temporal/polyfill"; -/** - * Type alias for the complete Temporal namespace. - * - * Use this type for function parameters, return types, and variable declarations - * when working with Temporal objects. This provides full IntelliSense support - * and type safety from `@js-temporal/polyfill`. - * - * @example - * ```typescript - * import { TemporalLike } from '@agape/temporal'; - * - * function processDate(temporal: TemporalLike) { - * const now = temporal.Now.plainDateTime('iso8601'); - * return now.toString(); - * } - * ``` - */ -export type TemporalLike = typeof Temporal; +let agapeTemporal: typeof TemporalPolyfill | undefined; + +const installedTemporal: typeof TemporalPolyfill = (globalThis as any)?.Temporal; + +// Get the temporal instance to use (agape first, then globalThis) +function getTemporalInstance(): typeof TemporalPolyfill | undefined { + return agapeTemporal ?? (globalThis as any)?.Temporal; +} + +function throwTemporalError() { + throw new Error( + `Temporal required. Use a JavaScript runtime which has Temporal or use a polyfill such as @js-temporal/polyfill or temporal-polyfill.` + ) +} + +class InstantStub { + constructor() { throwTemporalError() } + toString() { throwTemporalError() } + static from(value: string): InstantStub { return new InstantStub() } +} + +class ZonedDateTimeStub { + constructor() { throwTemporalError() } + toString() { throwTemporalError() } + static from(value: string): ZonedDateTimeStub { return new ZonedDateTimeStub() } +} + +class PlainDateStub { + constructor() { throwTemporalError() } + toString() { throwTemporalError() } + static from(value: string): PlainDateStub { return new PlainDateStub() } +} + +class PlainTimeStub { + constructor() { throwTemporalError() } + toString() { throwTemporalError() } + static from(value: string): PlainTimeStub { return new PlainTimeStub() } +} + +class PlainDateTimeStub { + constructor() { throwTemporalError() } + toString() { throwTemporalError() } + static from(value: string): PlainDateTimeStub { return new PlainDateTimeStub() } +} + +class PlainYearMonthStub { + constructor() { throwTemporalError() } + toString() { throwTemporalError() } + static from(value: string): PlainYearMonthStub { return new PlainYearMonthStub() } +} + +class PlainMonthDayStub { + constructor() { throwTemporalError() } + toString() { throwTemporalError() } + static from(value: string): PlainMonthDayStub { return new PlainMonthDayStub() } +} -let TemporalRef: TemporalLike | undefined; +class DurationStub { + constructor() { throwTemporalError() } + toString() { throwTemporalError() } + static from(value: string): DurationStub { return new DurationStub() } +} + +class TimeZoneStub { + constructor() { throwTemporalError() } + toString() { throwTemporalError() } + static from(value: string): TimeZoneStub { return new TimeZoneStub() } + getOffsetNanosecondsFor(instant: InstantStub): number { + throwTemporalError(); + return 0; + } +} /** - * Checks if Temporal is available in the current environment. - * - * This function detects both native Temporal (Node.js 20+, modern browsers) and - * polyfilled Temporal (when `@js-temporal/polyfill` is installed and configured). - * The result is cached for performance. - * - * @returns `true` if Temporal is available, `false` otherwise - * + * The Temporal namespace provides typed access to Temporal classes with intelligent fallback. + * + * This namespace automatically detects and uses the best available Temporal implementation: + * 1. Agape-level temporal (set via `setTemporal()`) + * 2. GlobalThis temporal (native or polyfill) + * 3. Stub implementation (throws helpful errors) + * + * All Temporal classes are available as properties on this namespace: + * - `Temporal.PlainDateTime` - Date and time without timezone + * - `Temporal.PlainDate` - Date without time or timezone + * - `Temporal.PlainTime` - Time without date or timezone + * - `Temporal.ZonedDateTime` - Date and time with timezone + * - `Temporal.Instant` - Single point in time + * - `Temporal.Duration` - Length of time + * - `Temporal.TimeZone` - Timezone representation + * * @example * ```typescript - * import { hasTemporal, getTemporal } from '@agape/temporal'; - * + * import { Temporal, hasTemporal } from '@agape/temporal'; + * * if (hasTemporal()) { - * const Temporal = getTemporal(); * const now = Temporal.PlainDateTime.from('2025-09-19T10:00'); * console.log(now.toString()); - * } else { - * console.warn('Temporal not available, falling back to Date'); * } * ``` */ -export function hasTemporal(): boolean { - if (TemporalRef) return true; - return "Temporal" in globalThis; +export namespace Temporal { + /** + * `Temporal.Instant` constructor or noop implementation. + * + * Represents a single point in time, independent of timezone. + * + * @see {@link https://tc39.es/proposal-temporal/docs/instant.html | The Temporal.Instant documentation} + * + * @example + * ```typescript + * const now = Temporal.Instant.from('2025-09-19T14:30:00Z'); + * const epoch = Temporal.Instant.fromEpochSeconds(0); + * console.log(now.toString()); // "2025-09-19T14:30:00Z" + * ``` + */ + export const Instant: typeof TemporalPolyfill.Instant = getTemporalInstance()?.Instant ?? InstantStub as any + + // 👇 This defines the type for usage like `TemporalStub.Instant` + export type Instant = TemporalPolyfill.Instant; + + /** + * `Temporal.ZonedDateTime` constructor or noop implementation. + * + * Represents a date and time with timezone information. + * + * @see {@link https://tc39.es/proposal-temporal/docs/zoneddatetime.html | The Temporal.ZonedDateTime documentation} + * + * @example + * ```typescript + * const nyTime = Temporal.ZonedDateTime.from('2025-09-19T14:30:00[America/New_York]'); + * const utcTime = nyTime.withTimeZone('UTC'); + * console.log(nyTime.toString()); // "2025-09-19T14:30:00-04:00[America/New_York]" + * ``` + */ + export const ZonedDateTime: typeof TemporalPolyfill.ZonedDateTime = getTemporalInstance()?.ZonedDateTime ?? ZonedDateTimeStub as any + + // 👇 This defines the type for usage like `TemporalStub.ZonedDateTime` + export type ZonedDateTime = TemporalPolyfill.ZonedDateTime; + + /** + * `Temporal.PlainDate` constructor or noop implementation. + * + * Represents a calendar date without time or timezone. + * + * @see {@link https://tc39.es/proposal-temporal/docs/plaindate.html | The Temporal.PlainDate documentation} + * + * @example + * ```typescript + * const date = Temporal.PlainDate.from('2025-09-19'); + * const tomorrow = date.add({ days: 1 }); + * console.log(tomorrow.toString()); // "2025-09-20" + * ``` + */ + export const PlainDate: typeof TemporalPolyfill.PlainDate = getTemporalInstance()?.PlainDate ?? PlainDateStub as any + + // 👇 This defines the type for usage like `TemporalStub.PlainDate` + export type PlainDate = TemporalPolyfill.PlainDate; + + /** + * `Temporal.PlainTime` constructor or noop implementation. + * + * Represents a time of day without date or timezone. + * + * @see {@link https://tc39.es/proposal-temporal/docs/plaintime.html | The Temporal.PlainTime documentation} + * + * @example + * ```typescript + * const time = Temporal.PlainTime.from('14:30:00'); + * const later = time.add({ hours: 2 }); + * console.log(later.toString()); // "16:30:00" + * ``` + */ + export const PlainTime: typeof TemporalPolyfill.PlainTime = getTemporalInstance()?.PlainTime ?? PlainTimeStub as any + + // 👇 This defines the type for usage like `TemporalStub.PlainTime` + export type PlainTime = TemporalPolyfill.PlainTime; + + /** + * `Temporal.PlainDateTime` constructor or noop implementation. + * + * Represents a date and time without timezone information. + * + * @see {@link https://tc39.es/proposal-temporal/docs/plaindatetime.html | The Temporal.PlainDateTime documentation} + * + * @example + * ```typescript + * const dt = Temporal.PlainDateTime.from('2025-09-19T14:30:00'); + * const nextWeek = dt.add({ weeks: 1 }); + * console.log(nextWeek.toString()); // "2025-09-26T14:30:00" + * ``` + */ + export const PlainDateTime: typeof TemporalPolyfill.PlainDateTime = getTemporalInstance()?.PlainDateTime ?? PlainDateTimeStub as any + + // 👇 This defines the type for usage like `TemporalStub.PlainDateTime` + export type PlainDateTime = TemporalPolyfill.PlainDateTime; + + /** + * `Temporal.PlainYearMonth` constructor or noop implementation. + * + * Represents a year and month without day information. + * + * @see {@link https://tc39.es/proposal-temporal/docs/plainyearmonth.html | The Temporal.PlainYearMonth documentation} + * + * @example + * ```typescript + * const ym = Temporal.PlainYearMonth.from('2025-09'); + * const nextMonth = ym.add({ months: 1 }); + * console.log(nextMonth.toString()); // "2025-10" + * ``` + */ + export const PlainYearMonth: typeof TemporalPolyfill.PlainYearMonth = getTemporalInstance()?.PlainYearMonth ?? PlainYearMonthStub as any + + // 👇 This defines the type for usage like `TemporalStub.PlainYearMonth` + export type PlainYearMonth = TemporalPolyfill.PlainYearMonth; + + /** + * `Temporal.PlainMonthDay` constructor or noop implementation. + * + * Represents a month and day without year information. + * + * @see {@link https://tc39.es/proposal-temporal/docs/plainmonthday.html | The Temporal.PlainMonthDay documentation} + * + * @example + * ```typescript + * const md = Temporal.PlainMonthDay.from('09-19'); + * const thisYear = md.toPlainDate({ year: 2025 }); + * console.log(thisYear.toString()); // "2025-09-19" + * ``` + */ + export const PlainMonthDay: typeof TemporalPolyfill.PlainMonthDay = getTemporalInstance()?.PlainMonthDay ?? PlainMonthDayStub as any + + // 👇 This defines the type for usage like `TemporalStub.PlainMonthDay` + export type PlainMonthDay = TemporalPolyfill.PlainMonthDay; + + /** + * `Temporal.Duration` constructor or noop implementation. + * + * Represents a length of time (duration). + * + * @see {@link https://tc39.es/proposal-temporal/docs/duration.html | The Temporal.Duration documentation} + * + * @example + * ```typescript + * const duration = Temporal.Duration.from('PT2H30M'); + * const doubled = duration.multiply(2); + * console.log(doubled.toString()); // "PT5H" + * ``` + */ + export const Duration: typeof TemporalPolyfill.Duration = getTemporalInstance()?.Duration ?? DurationStub as any + + // 👇 This defines the type for usage like `TemporalStub.Duration` + export type Duration = TemporalPolyfill.Duration; + + /** + * `Temporal.TimeZone` constructor or noop implementation. + * + * Represents a timezone. + * + * @see {@link https://tc39.es/proposal-temporal/docs/timezone.html | The Temporal.TimeZone documentation} + * + * @example + * ```typescript + * const tz = Temporal.TimeZone.from('America/New_York'); + * const offset = tz.getOffsetNanosecondsFor(Temporal.Instant.from('2025-09-19T14:30:00Z')); + * console.log(offset / 1_000_000_000 / 60); // -240 (minutes from UTC) + * ``` + */ + export const TimeZone: typeof TemporalPolyfill.TimeZone = getTemporalInstance()?.TimeZone ?? TimeZoneStub as any + + // 👇 This defines the type for usage like `TemporalStub.TimeZone` + export type TimeZone = TemporalPolyfill.TimeZone; } + /** - * Gets the Temporal object synchronously from the current environment. - * - * This function returns the native Temporal object if available, or a polyfilled - * version if one is installed and configured. If neither is available, it - * returns a typed stub that throws descriptive errors when accessed. - * - * The result is cached for performance, so subsequent calls return the same instance. - * This function is synchronous and safe to use in decorators and top-level initialization. - * - * @returns The Temporal object (native, polyfill, or typed stub) - * + * Sets the Temporal implementation to be used by the Agape Temporal namespace. + * + * This function allows you to explicitly configure which Temporal implementation + * should be used. The provided temporal takes precedence over any Temporal found + * on globalThis, giving you full control over the Temporal implementation used + * throughout your application. + * + * @param temporal - The Temporal implementation to use (e.g., from `@js-temporal/polyfill`) + * * @example - * ### Get the Temporal object * ```typescript - * import { getTemporal } from '@agape/temporal'; - * - * const Temporal = getTemporal(); + * import { setTemporal, Temporal } from '@agape/temporal'; + * import { Temporal as TemporalPolyfill } from '@js-temporal/polyfill'; + * + * // Set your preferred polyfill + * setTemporal(TemporalPolyfill); + * + * // Now all Temporal namespace usage will use this implementation * const now = Temporal.PlainDateTime.from('2025-09-19T10:00'); - * console.log(now.toString()); // "2025-09-19T10:00:00" + * console.log(now.toString()); // Uses your polyfill * ``` - * + * * @example - * - * # Configure a polyfill - * * ```typescript - * import { Temporal as TemporalPolyfill } from '@js-temporal/polyfill'; - * - * // Set globalThis.Temporal to make it available to getTemporal() - * (globalThis as any).Temporal = TemporalPolyfill; - * - * const Temporal = getTemporal(); - * const now = Temporal.PlainDateTime.from('2025-09-19T10:00'); - * console.log(now.toString()); // "2025-09-19T10:00:00" + * import { setTemporal, Temporal } from '@agape/temporal'; + * import { Temporal as CustomTemporal } from './my-custom-temporal'; + * + * // Use a custom Temporal implementation + * setTemporal(CustomTemporal); + * + * const date = Temporal.PlainDate.from('2025-09-19'); * ``` - * - * @throws {Error} When Temporal is not available and a property is accessed on the stub. - * The error message includes the property name and instructions for - * installing the polyfill or using a Temporal-enabled environment. */ -export function getTemporal(): TemporalLike { - if (TemporalRef) return TemporalRef; +export function setTemporal(temporal: typeof TemporalPolyfill): void { + agapeTemporal = temporal; + updateTemporalNamespace(); +} - if ("Temporal" in globalThis) { - TemporalRef = (globalThis as unknown as { Temporal: TemporalLike }).Temporal; - return TemporalRef; - } +/** + * Updates the Temporal namespace properties to point to the current temporal instance. + * This is called whenever setTemporal() or clearAgapeTemporal() is called. + */ +function updateTemporalNamespace(): void { + const currentTemporal = getTemporalInstance(); - // Return typed stub when Temporal is not available - TemporalRef = new Proxy({} as TemporalLike, { - get(_target, prop) { - throw new Error( - `Temporal is not available (accessed property: ${String(prop)}). ` + - `Install @js-temporal/polyfill or use Node 20+/modern browsers.` - ); - }, - }); - return TemporalRef; + // Update all properties to point to the current temporal instance or stubs + (Temporal as any).Instant = currentTemporal?.Instant ?? (InstantStub as any); + (Temporal as any).ZonedDateTime = currentTemporal?.ZonedDateTime ?? (ZonedDateTimeStub as any); + (Temporal as any).PlainDate = currentTemporal?.PlainDate ?? (PlainDateStub as any); + (Temporal as any).PlainTime = currentTemporal?.PlainTime ?? (PlainTimeStub as any); + (Temporal as any).PlainDateTime = currentTemporal?.PlainDateTime ?? (PlainDateTimeStub as any); + (Temporal as any).PlainYearMonth = currentTemporal?.PlainYearMonth ?? (PlainYearMonthStub as any); + (Temporal as any).PlainMonthDay = currentTemporal?.PlainMonthDay ?? (PlainMonthDayStub as any); + (Temporal as any).Duration = currentTemporal?.Duration ?? (DurationStub as any); + (Temporal as any).TimeZone = currentTemporal?.TimeZone ?? (TimeZoneStub as any); } + + +/** + * Checks if Temporal is available in the current environment. + * + * This function checks for Temporal implementations in the following priority order: + * 1. Agape-level temporal (set via `setTemporal()`) + * 2. GlobalThis temporal (native or polyfill) + * + * @returns `true` if Temporal is available, `false` otherwise + * + * @example + * ```typescript + * import { hasTemporal, Temporal } from '@agape/temporal'; + * + * if (hasTemporal()) { + * const now = Temporal.PlainDateTime.from('2025-09-19T10:00'); + * console.log(now.toString()); + * } else { + * console.warn('Temporal not available, falling back to Date'); + * } + * ``` + */ +export function hasTemporal(): boolean { + if (agapeTemporal) return true; + return "Temporal" in globalThis; +} + +// Initialize the namespace with the current temporal instance +updateTemporalNamespace();