Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
fe90cae
feat: expose the tzdb version of the bundled zonefile
dualfroz Oct 2, 2026
9c80369
fractional arithmetic
spencermountain Oct 7, 2026
fb144df
move tests, start temporal lib
spencermountain Oct 7, 2026
98e2ec9
temporal and tests reorg
spencermountain Oct 7, 2026
946b0bd
improve temporal
spencermountain Oct 7, 2026
65c18ff
all new tests passing
spencermountain Oct 7, 2026
c3dccf2
new tests
spencermountain Oct 7, 2026
ffaf3eb
new sticky invalid tests passing
spencermountain Oct 7, 2026
d92ecf0
dont copy input
spencermountain Oct 7, 2026
829ad4f
more input validation tests
spencermountain Oct 7, 2026
aef8900
bench test
spencermountain Oct 7, 2026
9155658
formatting doc
spencermountain Oct 8, 2026
1ba9f1c
doc
spencermountain Oct 9, 2026
c1b159b
doc
spencermountain Oct 9, 2026
029addc
doc
spencermountain Oct 9, 2026
1335a6f
doc
spencermountain Oct 9, 2026
cdda127
docs
spencermountain Oct 9, 2026
f99b9ca
formatting
spencermountain Oct 9, 2026
46fa24c
formatting
spencermountain Oct 9, 2026
f58efa2
temporal readme
spencermountain Oct 9, 2026
9090835
temporal cleanup, version outputs on plugins
spencermountain Oct 9, 2026
55ee4b2
Merge pull request #473 from dualfroz/fix/350-tzdb-version
spencermountain Oct 15, 2026
b6fbec9
fix(whereIts): return titlecased timezone names
dualfroz Oct 2, 2026
254ba61
Merge pull request #474 from dualfroz/fix/212-whereits-titlecase
spencermountain Oct 15, 2026
01515bf
changelog
spencermountain Oct 15, 2026
0eca167
gs
spencermountain Oct 16, 2026
a401d3f
lint plugins
spencermountain Oct 16, 2026
78a0d13
linting fixes
spencermountain Oct 16, 2026
2af0834
cleanup readme and geo plugin
spencermountain Oct 16, 2026
654c1d7
fixes to geo plugin and tests
spencermountain Oct 16, 2026
588007e
cleanup
spencermountain Oct 16, 2026
f6ad95e
cleanup docs and tests
spencermountain Oct 16, 2026
48ccdf9
rename timer plugin
spencermountain Oct 16, 2026
2e8ef6d
new plugin releases versions
spencermountain Oct 16, 2026
b64cd3b
7.17.0rc
spencermountain Oct 16, 2026
8ff7d8e
fix soltice tests
spencermountain Oct 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
73 changes: 55 additions & 18 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
@@ -1,36 +1,73 @@
name: Build and test

on: [pull_request]
on:
pull_request:
push:
branches: [master]
workflow_dispatch:
workflow_call:

permissions:
contents: read

jobs:
build:
test:
name: Node ${{ matrix.node-version }}
runs-on: ubuntu-latest

timeout-minutes: 15
strategy:
fail-fast: false
matrix:
node-version: [20.x, 26.x]
node-version: [18.x, 20.x, 22.x, 24.x, 26.x]
env:
CI: true

steps:
- uses: actions/checkout@v4
with:
persist-credentials: false

- uses: pnpm/action-setup@v4
with:
version: 10

- name: use node.js ${{ matrix.node-version }}
# Build tooling requires a newer Node than the library itself.
- name: Set up build runtime
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
node-version: 26.x
cache: pnpm
cache-dependency-path: pnpm-lock.yaml

- name: pnpm install, build, and test
run: |
pnpm install --frozen-lockfile
pnpm run lint
pnpm run pack
pnpm run test
pnpm run build
pnpm run testb
env:
CI: true
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build library
run: pnpm run build

- name: Build plugins
run: pnpm --filter './plugins/*' --if-present run build

- name: Set up test runtime
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}

- name: Test source
run: pnpm run test

- name: Test builds
run: pnpm run testb

- name: Test plugins
run: pnpm run test:plugins

- name: Test Temporal source
if: matrix.node-version == '26.x'
run: pnpm run test:temporal

- name: Check types
if: matrix.node-version == '26.x'
run: pnpm run test:types

- name: Lint
if: matrix.node-version == '26.x'
run: pnpm run lint
55 changes: 32 additions & 23 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,24 @@ name: Release

on:
release:
types: [created]
types: [published]

permissions:
contents: read

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
release:
validate:
if: github.event.release.prerelease == false
uses: ./.github/workflows/build-and-test.yml

publish:
needs: validate
runs-on: ubuntu-latest

timeout-minutes: 15
permissions:
id-token: write
contents: read
Expand All @@ -18,35 +30,32 @@ jobs:
persist-credentials: false

- uses: pnpm/action-setup@v4
with:
version: 10

- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
with:
node-version: 26.x
registry-url: 'https://registry.npmjs.org'
cache: pnpm
cache-dependency-path: pnpm-lock.yaml

- name: install
run: |
pnpm install --frozen-lockfile

- name: static checks
- name: Verify release version
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
pnpm run lint
node --input-type=module <<'NODE'
import assert from 'node:assert/strict'
import fs from 'node:fs'
const { version } = JSON.parse(fs.readFileSync('package.json', 'utf8'))
assert.equal(process.env.RELEASE_TAG.replace(/^v/, ''), version, 'Release tag must match package.json')
NODE

- name: build
run: |
pnpm run build
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: test
run: |
pnpm run test
pnpm run testb
- name: Build library
run: pnpm run build

- name: publish
run: |
pnpm publish --access public --provenance --no-git-checks
- name: Publish
run: pnpm publish --access public --provenance --no-git-checks
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ npm-debug.log
node_modules/
coverage/
coverage.lcov
**/bench/results.jsonl
114 changes: 51 additions & 63 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
# Spacetime

> spacetime is a zero-dependency JavaScript library for working with dates across
> timezones. It does timezone math (DST, leap years, hemispheres) **without** the
> `Intl` API, and exposes a Moment-like, mostly **immutable** API (see exceptions below).
> `Intl` API, and exposes a Moment-like, mostly **immutable** API.

See the [usage guide](./docs/README.md) for behavior and edge cases,
and the [API reference](./docs/API.md) for public signatures.

## Quick example

## Mental model
```js
import spacetime from 'spacetime'

Expand All @@ -22,82 +26,66 @@ const out = s.format('{day-short} {month} {date-ordinal}, {time}') // 'Thu March

## Docs

* [start here](./docs/README.md) - overview and task index
* [API](./docs/edge-cases.md) - full method list
* [inputs](./docs/inputs.md) - date/time parsing and supported formats
* [getters](./docs/getters.md) - getting specific datetime information
* [setters](./docs/setters.md) - modifying the datetime
* [formatting](./docs/formatting.md) - customizable output formats
* [timezones](./docs/timezones.md) - setting and changing iana zones
* [comparisons](./docs/comparisons.md) - utility functions on datetimes
* [edge cases](./docs/edge-cases.md) - DST, clamping, invalid inputs, and shared state
* [plugins](./docs/plugins.md) - extending or changing spacetime behaviour
- [start here](./docs/README.md) - overview and task index
- [API](./docs/API.md) - full method list
- [inputs](./docs/inputs.md) - date/time parsing and supported formats
- [getters](./docs/getters.md) - getting specific datetime information
- [setters](./docs/setters.md) - modifying the datetime
- [formatting](./docs/formatting.md) - customizable output formats
- [timezones](./docs/timezones.md) - setting and changing iana zones
- [comparisons](./docs/comparisons.md) - utility functions on datetimes
- [plugins](./docs/plugins.md) - extending or changing spacetime behaviour

---

## Important concepts

1. **Date arithmetic and most setters are immutable.** They return a *new*
`Spacetime` object. Capture the return value. Exceptions: `epochSeconds(value)`
and `weekStart(value)` mutate the instance; `i18n()` changes shared language data.
`s.add(1, 'day')` alone does nothing observable — write
`s = s.add(1, 'day')`.

2. **Months are 0-based; dates (day-of-month) are 1-based.** `s.month()` returns
`0` for January … `11` for December. `s.date()` returns `1`–`31`. This mirrors
the JavaScript `Date` spec. Array input follows the same rule:
`spacetime([2017, 5, 2])` is **June 2nd 2017**, not May.

3. **Epochs are in MILLISECONDS.** `spacetime(1489520157124)` is a millisecond
epoch. If you only have seconds, use `spacetime.fromUnixSeconds(seconds)` or
multiply by 1000. Passing a seconds-epoch silently lands you in January 1970.

4. **Check each timezone signature.** The constructor and `fromUnixSeconds`
take `(input, timezone?, options?)`; `now`, `today`, `tomorrow`, `yesterday`,
`min`, and `max` take `(timezone?, options?)`. If you omit the timezone,
the *local machine/browser* timezone is used. Use IANA names
(`'America/New_York'`, `'Europe/Paris'`).

5. **Getters and setters are the same method name.** Calling with no argument
*reads*; calling with an argument *returns a new object* with that value set.
`s.hour()` → `14`; `s.hour(9)` → a new Spacetime at 9am. This dual signature
exists for: `millisecond, second, minute, hour, hour12, date, month, year,
day, dayName, dayOfYear, time, week, quarter, season, hourFloat, ampm,
dayTime, monthName, decade, century, millennium, json, iso,
isoFull, timezone`. `epochSeconds()` also has a setter, but it mutates the instance.
## Repository map

---
- `src/` — regular entry, instance implementation, parsers (`input/`), public
methods (`methods/`), and timezone calculations (`timezone/`).
- `src/temporal/` — experimental Temporal-backed entry.
- `zonefile/` — timezone data and packing; `scripts/tz/` updates the data.
- `builds/` — generated distribution bundles; change source rather than bundles.
- `types/` — public declarations;
- `tests/` — Tape suites grouped by behavior; `plugins/` contains workspace packages.

## Checks and builds

Run checks relevant to the change. Use a runtime with `globalThis.Temporal` for
Temporal tests; CI uses Node 26 for tooling and tests the regular library on
Node 18, 20, 22, 24, and 26.

## Common mistakes
- setter immutability - `s.add(1, 'day')` does not modify `s`
- `s.month()` returns numeric `0`–`11`
- Use `s.monthName()` for the string, `s.format('month')` for 'April'.
- array input - `spacetime([2017, 5, 2])` "= May 2" → it is **June 2** (month is 0-based).
- Passing a seconds epoch to `spacetime(...)` and getting date in ~1970.
- use `spacetime.fromUnixSeconds(...)`.
- Putting the timezone first
- constructor timezone is the **2nd** argument; `now(tz)` takes it first.
- Using `goto()` when you meant `timezone()` (or vice versa): `goto(tz)` keeps
the instant; `timezone(tz)` keeps the wall-clock values. See [timezones](./docs/timezones.md).
- Expecting `s.epoch()` / `s.tz()` as methods → `epoch` and `tz` are
**properties** (no parentheses). `epochSeconds()` *is* a method.
| Command | Purpose |
| --------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `pnpm test` | Main test suite |
| `pnpm lint` | ESLint |
| `pnpm test:types` | Regular-entry declaration checks |
| `pnpm test:temporal` | Focused Temporal suite |
| `pnpm test:plugins` | Plugin suites |
| `pnpm build` | Pack timezone data, update `src/_version.js`, and build both entries |
| `pnpm testb` | Main suite against existing bundles; build first to include source changes |

The [CI workflow](./.github/workflows/build-and-test.yml) also checks Temporal
bundles. `pnpm test:all` runs source, Temporal, types, plugins, and lint; it does
not build or include every CI check.

---

# Development
## Development

Unless given specific instruction:

- Do not edit README or add documentation
- do not install or change dependencies
- do not change existing tests
- do not make a commit or PR

Work on the current branch. The user may make simultaneous changes. Verify their work is not overwritten, or ask permission before destructive git changes.

### Code style
- Write maintainable javascript, using esmodules
- Write portable ES2022+ for browers and Node>=18
- Typescript and jsdoc are not required
## Code style

- Write maintainable JavaScript, using ES modules
- Write portable ES2022+ for browsers and Node >=18
- TypeScript and JSDoc are not required
- Add terse comments for maintainability
- Prefer functions assigned with const, over declarations
- Do not use unbracketed if statements
Expand All @@ -107,9 +95,9 @@ Work on the current branch. The user may make simultaneous changes. Verify their
- File-size is always important
- Defensive try/catch blocks are not required

### Project structure
## File conventions

- Prefer pnpm over npm
- eslint is always configured
- Prefer small maintainable files with one purpose
- Split out utility functions into a _lib.js file or ./_lib dir
- Prefer `export default` on files with one export
Expand Down
Loading
Loading