# Day.js Admin & Architecture Guide

This guide is the source-based internals reference for Day.js: how the core engine is structured, how the plugin system rewrites prototype methods at runtime, how parsing and formatting actually work, and what to know to build, test, secure, and contribute to the library. It is written for maintainers, integrators embedding Day.js deep in a toolchain, and architects evaluating it as a Moment.js replacement.

For end-user API usage, see the companion **User Guide**.

---

## Table of Contents

1. [Executive Summary](#1-executive-summary)
2. [Architecture Overview](#2-architecture-overview)
3. [Component Inventory](#3-component-inventory)
4. [The Core Engine](#4-the-core-engine)
5. [The Plugin System](#5-the-plugin-system)
6. [The Parsing Pipeline](#6-the-parsing-pipeline)
7. [The Formatting Pipeline](#7-the-formatting-pipeline)
8. [Immutability and the Wrapper Model](#8-immutability-and-the-wrapper-model)
9. [The Locale System](#9-the-locale-system)
10. [Requirements](#10-requirements)
11. [Build and Release Pipeline](#11-build-and-release-pipeline)
12. [Testing Strategy](#12-testing-strategy)
13. [Security Analysis](#13-security-analysis)
14. [Code Quality and Patterns](#14-code-quality-and-patterns)
15. [Internal Field Reference](#15-internal-field-reference)
16. [Potential Improvements and Tech Debt](#16-potential-improvements-and-tech-debt)
17. [Contributing](#17-contributing)
18. [Quick Reference](#18-quick-reference)

---

## 1. Executive Summary

Day.js is a small, immutable date-time library that wraps the native JavaScript `Date` object and exposes a Moment.js-compatible API. Its defining architectural choices are:

- **A tiny, fixed core.** The entire core is three files — `src/constant.js`, `src/utils.js`, `src/index.js` — totaling roughly 14&nbsp;KB of source and enforced to ship under **2.99&nbsp;KB** minified+gzipped by a `size-limit` budget.
- **Zero runtime dependencies.** Every entry in `package.json` is a `devDependency`. Nothing is pulled into a consumer's bundle except Day.js itself.
- **Everything optional is a plugin.** Relative time, UTC, time zones, durations, custom parsing, ISO weeks, and 30+ more capabilities are delivered as **37 plugins** that monkey-patch the prototype at runtime via `dayjs.extend()`.
- **Immutability by default.** Manipulation methods return new instances through a single `wrapper()` factory. Mutation is available only through the deliberately-named `badMutable` plugin.
- **On-demand i18n.** **143 locale files** ship with the package; only English is bundled into the core, and other locales register themselves on import.

| Attribute | Value |
|-----------|-------|
| Language | JavaScript (ES2015+), authored in `src/`, transpiled with Babel |
| Type definitions | Bundled (`types/index.d.ts` + per-plugin `.d.ts`) |
| Runtime dependencies | **None** |
| Core size budget | 2.99&nbsp;KB (`dayjs.min.js`, gzipped) |
| Plugins | 37 official plugins under `src/plugin/` |
| Locales | 143 files under `src/locale/` |
| License | MIT |
| Module formats | UMD (`dayjs.min.js`), ESM (`esm/`) |
| Test runner | Jest, 100% line-coverage threshold |
| Release | Automated via semantic-release |

---

## 2. Architecture Overview

```
                         ┌─────────────────────────────────────────┐
                         │            Consumer application            │
                         │   import dayjs from 'dayjs'                │
                         │   dayjs.extend(plugin)                     │
                         └───────────────────┬───────────────────────┘
                                             │
            ┌────────────────────────────────┼────────────────────────────────┐
            │                  CORE  (src/, ~2KB, no deps)                       │
            │                                                                    │
            │   ┌──────────────┐   ┌────────────┐   ┌──────────────────────┐    │
            │   │ constant.js  │   │  utils.js  │   │      index.js        │    │
            │   │ time units,  │──▶│ s,z,m,a,p,u│──▶│  dayjs() factory     │    │
            │   │ regexes,     │   │ (1-letter  │   │  class Dayjs         │    │
            │   │ FORMAT_DEF.  │   │  aliases)  │   │  wrapper(), parse,   │    │
            │   └──────────────┘   └────────────┘   │  format, diff, …     │    │
            │                                        │  Ls{} locale registry│    │
            │                          built-in ───▶ │  en locale           │    │
            │                                        └──────────┬───────────┘    │
            └───────────────────────────────────────────────── │ ───────────────┘
                                                                │ dayjs.extend()
                       ┌────────────────────────────────────────┼───────────────────┐
                       │                                         │                   │
              ┌────────▼─────────┐                     ┌─────────▼────────┐  ┌───────▼────────┐
              │  PLUGINS (37)     │                     │  LOCALES (143)    │  │  TYPES          │
              │ src/plugin/*      │                     │ src/locale/*      │  │ types/*.d.ts    │
              │ monkey-patch      │                     │ register into Ls  │  │ augment module  │
              │ Dayjs.prototype   │                     │ on import         │  │ 'dayjs'         │
              │ or dayjs.* static │                     │                   │  │                 │
              └───────────────────┘                     └───────────────────┘  └─────────────────┘
```

The core never imports a plugin or a non-English locale. Dependencies flow strictly inward: `index.js` depends on `utils.js` and `constant.js`; plugins and locales depend on the public surface the core exposes to them at extend/import time. This is what keeps the core's size constant regardless of how many plugins exist.

### Data flow for a typical call

```
dayjs('2019-01-25')
  → factory: not a Dayjs, build cfg { date, args }
  → new Dayjs(cfg)
      → parseLocale(cfg.locale) sets $L
      → parse(cfg) → parseDate(cfg) → native Date → $d
      → init() caches $y,$M,$D,$W,$H,$m,$s,$ms
  → .add(1, 'day')
      → computes next timestamp
      → wrapper(ts, this) → dayjs(ts, {locale,$u,$x,$offset})
      → NEW Dayjs instance (original untouched)
  → .format('YYYY-MM-DD')
      → REGEX_FORMAT.replace with matches() lookup
      → string
```

---

## 3. Component Inventory

| Path | Role |
|------|------|
| `src/constant.js` | Time-unit milliseconds, unit string constants, `FORMAT_DEFAULT`, parse/format regexes, `INVALID_DATE_STRING` |
| `src/utils.js` | Shared utility object with single-letter keys consumed by core and plugins |
| `src/index.js` | `dayjs()` factory, `Dayjs` class, `wrapper()`, locale registry, `extend()`, static helpers |
| `src/locale/*.js` | 143 locale definition modules (English built into the core) |
| `src/plugin/*/index.js` | 37 plugin modules, each a `(option, Dayjs, dayjs) => void` function |
| `src/plugin/localizedFormat/utils.js` | Shared helpers (`u`, `t`, `englishFormats`) used by `localizedFormat`, `localeData`, `customParseFormat` |
| `types/index.d.ts` | Core TypeScript declarations and the `PluginFunc` contract |
| `types/plugin/*.d.ts` | Per-plugin declarations that augment the `dayjs` module |
| `types/locale/*.d.ts` | Locale module declarations |
| `test/*.test.js` | Core feature tests |
| `test/plugin/*.test.js` | One test file per plugin |
| `test/locale/*.test.js` | Locale-specific tests |
| `test/issues/*.test.js` | Regression tests pinned to GitHub issues |
| `build/` | Rollup/Babel build scripts (referenced by `npm run build`) |
| `docs/` | Multilingual documentation site source (en, zh-cn, ja, ko, pt-br, es-es, ru, fr, he, si, sw, tr) |

> [!NOTE]
> The build directory and bundler config (`karma.sauce.conf.js`, `babel.config.js`, `prettier.config.js`, `.eslintrc.json`, `.releaserc`) live at the repo root. The published artifacts (`dayjs.min.js`, `esm/`, individual plugin/locale files) are generated, not committed to `src/`.

---

## 4. The Core Engine

### `src/constant.js` — the vocabulary

This module defines everything the rest of the library treats as fixed:

- **Millisecond constants** — `MILLISECONDS_A_SECOND` (1e3) through `MILLISECONDS_A_WEEK`, derived from `SECONDS_A_*`. Used by `add`, `diff`, and the `duration` plugin.
- **Unit string constants** — `MS`, `S`, `MIN`, `H`, `D`, `W`, `M`, `Q`, `Y`, `DATE`. These are the canonical internal unit names (`'millisecond'`, `'second'`, … `'date'`). Every method normalizes incoming units to one of these.
- **`FORMAT_DEFAULT`** = `'YYYY-MM-DDTHH:mm:ssZ'` — the template used when `format()` is called with no argument.
- **`INVALID_DATE_STRING`** = `'Invalid Date'` — the sentinel `isValid()` checks against.
- **`REGEX_PARSE`** — matches ISO 8601-like strings into capture groups (year, month, day, hour, minute, second, milliseconds).
- **`REGEX_FORMAT`** — matches format tokens (and bracketed escapes) during `format()`.

### `src/utils.js` — the toolbox

A single default-exported object whose keys are one letter each, to minimize minified size. These are attached to the shared `Utils` object and reused by every plugin:

| Key | Function | Purpose |
|-----|----------|---------|
| `s` | `padStart` | Left-pad numbers (zero-padding for `MM`, `DD`, …) |
| `z` | `padZoneStr` | Build the `±HH:mm` zone string for the `Z` token |
| `m` | `monthDiff` | Month difference, ported verbatim from Moment.js for parity |
| `a` | `absFloor` | Floor toward zero (used by `diff`) |
| `p` | `prettyUnit` | Normalize a unit alias to its canonical constant |
| `u` | `isUndefined` | `x === undefined` check (used everywhere as a guard) |

> [!IMPORTANT]
> The single-letter naming is a deliberate size optimization, not obfuscation. Plugins reference these as `this.$utils().s(...)`, `utils.p(...)`, etc. When reading plugin source, keep this table handy — `u` is "is undefined," `p` is "pretty unit," `s` is "pad start."

At load time, `index.js` augments this object with three more functions that need closure over core internals:

- `Utils.l = parseLocale` — locale resolution
- `Utils.i = isDayjs` — type predicate
- `Utils.w = wrapper` — the immutable instance factory

### `src/index.js` — the engine

This file defines:

- **`dayjs(date, c)`** — the factory. If `date` is already a Day.js object it returns a clone; otherwise it builds a config object (`{ date, args }`, merging any options in `c`) and constructs a `Dayjs`.
- **`class Dayjs`** — the instance type. The constructor resolves the locale, parses the date, seeds the `$x` extension bag, and tags the instance with `$isDayjsObject`.
- **`wrapper(date, instance)`** — creates a new `dayjs` carrying forward `locale`, `utc` (`$u`), extension data (`$x`), and `$offset`. This is the single chokepoint through which all immutable results are produced.
- **`parseDate(cfg)`** — the core parser (see [§6](#6-the-parsing-pipeline)).
- **The locale registry** — module-scoped `L` (current global locale name, defaults to `'en'`) and `Ls` (a map of name → locale object), seeded with the built-in English locale.
- **Getter/setter generation** — an array maps internal fields (`$y`, `$M`, …) to unit constants and defines each prototype accessor through the shared `$g` helper, so `year()`, `month()`, etc. are generated rather than hand-written.
- **Static surface** — `dayjs.extend`, `dayjs.locale`, `dayjs.isDayjs`, `dayjs.unix`, `dayjs.en`, `dayjs.Ls`, and `dayjs.p` (a plugin-flag registry).

The `Dayjs` prototype methods grouped by concern:

| Concern | Methods |
|---------|---------|
| Lifecycle | `constructor`, `parse`, `init`, `clone` |
| Validity | `isValid` |
| Get/Set | `$g`, `$set`, `set`, `get`, generated unit accessors, `daysInMonth` |
| Manipulate | `add`, `subtract`, `startOf`, `endOf` |
| Display | `format`, `utcOffset`, `diff`, `toDate`, `toJSON`, `toISOString`, `toString`, `valueOf`, `unix` |
| Compare | `isSame`, `isBefore`, `isAfter` |
| Locale | `$locale`, `locale` |
| Plugin hook | `$utils` |

---

## 5. The Plugin System

The plugin system is the heart of Day.js's "small core, opt-in everything" design.

### The contract

A plugin is a function with this TypeScript signature:

```ts
export type PluginFunc<T = unknown> =
  (option: T, c: typeof Dayjs, d: typeof dayjs) => void
```

It receives three arguments:

1. `option` — the optional second argument passed to `dayjs.extend(plugin, option)`.
2. `c` — the **`Dayjs` class** (so the plugin can patch `c.prototype`).
3. `d` — the **`dayjs` factory** (so the plugin can add static methods or construct new instances).

### Installation — `dayjs.extend`

```js
dayjs.extend = (plugin, option) => {
  if (!plugin.$i) {           // install only once
    plugin(option, Dayjs, dayjs)
    plugin.$i = true
  }
  return dayjs
}
```

> [!IMPORTANT]
> The `plugin.$i` flag guards against double installation — calling `extend` with the same plugin twice is a no-op. Because the flag lives on the plugin function object, it is process-global. This makes `extend` idempotent but also means a plugin's patches persist for the lifetime of the module.

### How plugins extend behavior

Plugins use three techniques, often in combination:

**1. Add a new prototype method:**

```js
// isLeapYear
export default (o, c) => {
  c.prototype.isLeapYear = function () {
    return ((this.$y % 4 === 0) && (this.$y % 100 !== 0)) || (this.$y % 400 === 0)
  }
}
```

**2. Wrap (decorate) an existing method:**

```js
// quarterOfYear extending add()
const oldAdd = proto.add
proto.add = function (number, units) {
  const unit = this.$utils().p(units)
  if (unit === Q) return this.add(number * 3, M)
  return oldAdd.bind(this)(number, units)   // delegate to the original
}
```

This "save old, define new, delegate to old" pattern is how `customParseFormat`, `utc`, `objectSupport`, `arraySupport`, `bigIntSupport`, `negativeYear`, `badMutable`, `isoWeek`, `timezone`, and others layer onto core methods like `parse`, `format`, `startOf`, `add`, and `diff`.

**3. Add a static method to the factory:**

```js
// minMax
d.max = function () { /* … */ }
d.min = function () { /* … */ }
```

Statics added this way include `dayjs.duration`/`isDuration`, `dayjs.max`/`min`, `dayjs.utc`, `dayjs.tz`, `dayjs.updateLocale`, `dayjs.months`/`weekdays`, and `dayjs.isMoment`.

### The extension data bag — `$x`

Plugins that need to attach per-instance state use the `$x` object, which `wrapper()` propagates across immutable operations. For example, the `timezone` plugin stores `$x.$timezone`, and `utc` stores `$x.$localOffset`. This is the sanctioned way to add instance state without new top-level fields.

### The plugin flag registry — `dayjs.p`

`dayjs.p` is an empty object the core exposes so plugins can advertise their presence to one another. `customParseFormat` sets `d.p.customParseFormat = true`; the `devHelper` plugin reads that flag to decide whether to warn about format-string parsing.

### Method-override ordering matters

Because plugins wrap methods in install order, the sequence of `extend` calls can change behavior when two plugins patch the same method.

> [!WARNING]
> Several plugins patch the same core methods (`parse`, `format`, `startOf`, `add`, `diff`). When two such plugins interact, **extend order determines wrapping order**. Known hard dependencies: `timezone` requires `utc`; `weekYear` requires `weekOfYear`; `isoWeeksInYear` requires `isLeapYear`; `duration.humanize()` requires `relativeTime`. Establish a single, ordered `extend` block at startup.

### Plugin catalog

| Plugin | Technique | Patches / Adds |
|--------|-----------|----------------|
| `advancedFormat` | wrap | `format` (extra tokens) |
| `arraySupport` | wrap | `parse` (array input) |
| `badMutable` | wrap | `$g`, `set`, `startOf`, `add`, `locale`, `daysInMonth`, `isSame/Before/After` → mutate in place |
| `bigIntSupport` | wrap | `parse`, `dayjs.unix` |
| `buddhistEra` | wrap | `format` (`BBBB`/`BB`) |
| `calendar` | add | `calendar()` |
| `customParseFormat` | wrap | `parse` (format strings, strict mode, format arrays) |
| `dayOfYear` | add | `dayOfYear()` |
| `devHelper` | wrap | `parse`, `diff`, `dayjs.locale` (dev warnings) |
| `duration` | add + wrap | `dayjs.duration`, `isDuration`; `add`/`subtract` accept durations |
| `isBetween` | add | `isBetween()` |
| `isLeapYear` | add | `isLeapYear()` |
| `isMoment` | add | `dayjs.isMoment` |
| `isoWeek` | add + wrap | `isoWeek*`, `startOf('isoWeek')` |
| `isoWeeksInYear` | add | `isoWeeksInYear()` |
| `isSameOrAfter` / `isSameOrBefore` | add | comparison helpers |
| `isToday` / `isTomorrow` / `isYesterday` | add | relative-day checks |
| `localeData` | add | instance + static `localeData()`, `months`, `weekdays*` |
| `localizedFormat` | wrap | `format` (`L`/`LL`/`LLLL`) |
| `minMax` | add | `dayjs.max`, `dayjs.min` |
| `negativeYear` | wrap | `parse` (negative-year strings) |
| `objectSupport` | wrap | `parse`, `set`, `add`, `subtract` (object input) |
| `pluralGetSet` | add | plural accessor aliases |
| `preParsePostFormat` | wrap | `parse`, `format`, `fromToBase` (locale hooks) |
| `quarterOfYear` | add + wrap | `quarter()`, `'quarter'` in `add`/`startOf` |
| `relativeTime` | add | `from`, `to`, `fromNow`, `toNow`, `fromToBase` |
| `timezone` | add + wrap | `tz`, `offsetName`, `startOf`; `dayjs.tz*` |
| `toArray` / `toObject` | add | serializers |
| `updateLocale` | add | `dayjs.updateLocale` |
| `utc` | add + wrap | `dayjs.utc`; `parse`, `init`, `utcOffset`, `format`, `valueOf`, `diff`, `toDate`, … |
| `weekday` | add | `weekday()` |
| `weekOfYear` | add | `week()`, `weeks()` |
| `weekYear` | add | `weekYear()` |

---

## 6. The Parsing Pipeline

The core `parseDate(cfg)` resolves input to a native `Date` in this order:

1. **`null`** → `new Date(NaN)` (explicitly invalid).
2. **`undefined`** → `new Date()` (now).
3. **`Date` instance** → `new Date(date)` (defensive copy).
4. **String without a trailing `Z`** → matched against `REGEX_PARSE`. On a match, components are extracted (month is decremented because `Date` months are zero-indexed; milliseconds are truncated to three digits) and a local or UTC `Date` is constructed.
5. **Everything else** (numbers, `Z`-suffixed strings, unmatched strings) → `new Date(date)`, delegating to the native engine.

```js
const parseDate = (cfg) => {
  const { date, utc } = cfg
  if (date === null) return new Date(NaN)
  if (Utils.u(date)) return new Date()
  if (date instanceof Date) return new Date(date)
  if (typeof date === 'string' && !/Z$/i.test(date)) {
    const d = date.match(C.REGEX_PARSE)
    if (d) { /* build Date from capture groups */ }
  }
  return new Date(date)
}
```

> [!CAUTION]
> Step 5 is the source of cross-engine inconsistency: an unmatched string falls through to `new Date(string)`, whose parsing of non-ISO formats is implementation-defined. This is exactly the gap the `customParseFormat` plugin closes by replacing `parse` entirely for format-string input.

### Plugin-extended parsing

Each parse-extending plugin wraps `proto.parse`, transforms `cfg.date` (or replaces `$d` directly), and delegates to the original:

- `arraySupport` — array → `Date` via `Date.UTC.apply` or the `Date` constructor.
- `objectSupport` — `{ year, month, … }` → `Date`.
- `bigIntSupport` — BigInt → `Number`.
- `negativeYear` — leading `-` year strings.
- `customParseFormat` — tokenized format parsing with optional strict validation and format-array fallthrough.
- `utc` — sets the `$u` flag and switches `init()` to `getUTC*` accessors.

After `parseDate`, `init()` caches the broken-down components (`$y`, `$M`, `$D`, `$W`, `$H`, `$m`, `$s`, `$ms`) so accessors are field reads, not repeated `Date` method calls.

---

## 7. The Formatting Pipeline

`format(formatStr)` works as follows:

1. If the instance is invalid, return the locale's `invalidDate` or `'Invalid Date'`.
2. Default the template to `FORMAT_DEFAULT` (`YYYY-MM-DDTHH:mm:ssZ`).
3. Compute the zone string with `Utils.z`.
4. Run `formatStr.replace(REGEX_FORMAT, …)`. The replacer:
   - Returns bracketed escapes (`[literal]`) verbatim.
   - Looks up each token in the `matches()` switch (`YY`, `YYYY`, `M`…`SSS`, `Z`).
   - Falls back to the zone string with the colon stripped (the `ZZ` case).

Name-based tokens (`MMM`, `MMMM`, `dd`, `ddd`, `dddd`, `A`/`a`) read from the active locale, with `getShort` slicing full names when a locale omits explicit short/min variants. The meridiem defaults to AM/PM when the locale supplies no `meridiem` function.

Format-extending plugins wrap `format` and pre-process the template before delegating to the core:

- `localizedFormat` expands `L`/`LL`/`LLLL` (and lowercase variants) using the locale's `formats` map and the English fallbacks in `localizedFormat/utils.js`.
- `advancedFormat` handles `Q`, `Do`, `X`, `x`, `w`/`W`, `k`, `z`, etc., then calls the old `format`.
- `buddhistEra` substitutes `BBBB`/`BB`.
- `preParsePostFormat` runs the locale's `postformat` on the result (for non-Latin numerals).
- `utc` changes the default template to `YYYY-MM-DDTHH:mm:ss[Z]` in UTC mode.

> [!NOTE]
> Because format plugins wrap rather than replace, layering (e.g. `localizedFormat` + `advancedFormat` + `buddhistEra`) composes: each plugin transforms the template or output and hands off to the next-inner `format`.

---

## 8. Immutability and the Wrapper Model

Immutability is enforced structurally, not by freezing objects. Every method that produces a "changed" date routes through `wrapper()`:

```js
const wrapper = (date, instance) =>
  dayjs(date, {
    locale: instance.$L,
    utc: instance.$u,
    x: instance.$x,
    $offset: instance.$offset
  })
```

`wrapper(newTimestamp, this)` constructs a brand-new `Dayjs` that inherits the originating instance's locale, UTC flag, extension bag, and offset. The original is never touched.

- `add`, `subtract`, `startOf`, `endOf`, `set`, `clone`, and the unit setters all return `wrapper(...)` results.
- `$set` (the private setter) mutates a clone, never `this` directly in the public path — `set()` calls `this.clone().$set(...)`.

> [!IMPORTANT]
> The `badMutable` plugin breaks this contract intentionally. It redefines `set`, `startOf`, `add`, `locale`, etc. to assign back to `this.$d` and call `this.init()`, returning `this`. This is the only supported way to get Moment.js-style mutation, and it should be confined to migration code.

There is a known internal wart here: the `$offset` field carried by `wrapper` is annotated in source with `// todo: refactor; do not use this.$offset in you code`. See [§16](#16-potential-improvements-and-tech-debt).

---

## 9. The Locale System

### Registry and resolution

Two module-scoped values in `index.js` hold all locale state:

- `L` — the current **global** locale name (`'en'` by default).
- `Ls` — a map of locale name → locale object, seeded with English.

`parseLocale(preset, object, isLocal)` is the single resolution function:

- With no preset, returns the current global `L`.
- With a string preset, lowercases it, registers `object` if provided, and falls back to the language part of a `lang-region` code if the full code isn't loaded (e.g. `en-xyz` → `en`).
- With an object preset, registers it under its `name`.
- Unless `isLocal` is set, it updates the global `L`.

This is exposed three ways: `dayjs.locale()` (global get/set), `instance.locale()` (per-object, always local — returns a clone), and `Utils.l` for plugin use.

### Locale object shape

A locale module default-exports an object. The built-in English locale is intentionally minimal:

```js
// src/locale/en.js
export default {
  name: 'en',
  weekdays: 'Sunday_Monday_…_Saturday'.split('_'),
  months: 'January_…_December'.split('_'),
  ordinal: (n) => { /* 1st, 2nd, 3rd, … */ }
}
```

Richer locales add `weekdaysShort`, `weekdaysMin`, `monthsShort`, `weekStart`, `yearStart`, `formats` (for `localizedFormat`), `relativeTime` (for `relativeTime`), `meridiem`, and `calendar`. English omits the short/min name arrays because `format()` derives them by slicing the full names when explicit variants are absent.

> [!NOTE]
> Non-English locale files `import dayjs from 'dayjs'` and call `dayjs.locale(name, localeObject, true)` on load, registering themselves into `Ls` without changing the global default. That self-registration on import is why `import 'dayjs/locale/zh-cn'` is sufficient to make the locale available.

### Mutating locales

The `updateLocale` plugin merges a partial config into an already-registered locale, deep-merging object-valued keys (like `formats`) and replacing others. It returns `undefined` if the locale isn't loaded.

> [!CAUTION]
> `updateLocale` mutates the shared locale object in `Ls` in place. Changes are global and persist for the process. Treat locale tweaks as application-startup configuration, not per-request state.

---

## 10. Requirements

### Runtime (consumers)

| Requirement | Detail |
|-------------|--------|
| JavaScript engine | Any environment with a working native `Date`. The README states all browsers are supported. |
| `Intl.DateTimeFormat` | Required **only** for the `timezone` plugin (IANA zone math and `offsetName`). |
| `BigInt` | Required only for the `bigIntSupport` plugin. |
| Module system | UMD build works as a global, CommonJS `require`, or AMD; ESM build is published under `esm/`. |
| Runtime dependencies | None. |

### Development (contributors)

| Tool | Source of truth |
|------|-----------------|
| Node.js + npm | Standard; the toolchain is npm-script driven |
| Babel | `@babel/cli`, `@babel/core`, `@babel/preset-env` — transpiles `src/` |
| Rollup | `rollup` + `rollup-plugin-babel` + `rollup-plugin-terser` — bundles `dayjs.min.js` |
| Jest | `jest` + `babel-jest` — tests with coverage |
| ESLint | `eslint` + `eslint-config-airbnb-base` + plugins |
| Prettier | `prettier` — formats `docs/**/*.md` |
| size-limit | enforces the 2.99&nbsp;KB budget |
| TypeScript | `typescript` — validates the bundled `.d.ts` files |
| Karma + Sauce | cross-browser test matrix |
| MockDate, moment, moment-timezone | test fixtures and parity comparisons |

---

## 11. Build and Release Pipeline

All build orchestration lives in `package.json` scripts:

| Script | Command | Purpose |
|--------|---------|---------|
| `build` | `cross-env BABEL_ENV=build node build && npm run size` | Bundle the UMD `dayjs.min.js`, then check size |
| `babel` | `cross-env BABEL_ENV=build babel src --out-dir esm --copy-files && node build/esm` | Produce the ESM build under `esm/` |
| `size` | `size-limit && gzip-size dayjs.min.js` | Enforce the 2.99&nbsp;KB budget |
| `test` | Multi-timezone Jest run + 100% line-coverage gate | Correctness across zones |
| `test-tz` | `date && jest test/timezone.test --coverage=false` | Timezone suite under a forced `TZ` |
| `lint` | `eslint src/* test/* build/*` | Style and correctness linting |
| `prettier` | `prettier --write "docs/**/*.md"` | Format documentation |
| `sauce` / `test:sauce` | `karma start karma.sauce.conf.js` | Cross-browser testing via Sauce Labs |

### Size budget

```json
"size-limit": [
  { "limit": "2.99 KB", "path": "dayjs.min.js" }
]
```

> [!IMPORTANT]
> The 2.99&nbsp;KB ceiling on `dayjs.min.js` is a hard CI gate. Any core change that pushes the bundle over budget fails the build. This is the mechanism that keeps the "2&nbsp;KB" promise honest — it is why new features are added as plugins rather than to the core.

### Pre-commit and release

- A `pre-commit` hook runs `lint` before every commit.
- Releases are automated with **semantic-release** (`@semantic-release/github`, configured via `.releaserc`). Version numbers, changelog entries, and GitHub releases are derived from conventional commit messages — note `package.json`'s `version` is the placeholder `"0.0.0-development"` because the real version is computed at release time.

> [!NOTE]
> The repo carries a local patch (`patches/@semantic-release+github+11.0.4.patch`) applied to the release tooling. Patches like this are applied via the package manager's patch mechanism during install.

---

## 12. Testing Strategy

Day.js holds a **100% line-coverage threshold** and tests every public surface.

```json
"jest": {
  "roots": ["test"],
  "testRegex": "test/(.*?/)?.*test.js$",
  "collectCoverage": true,
  "collectCoverageFrom": ["src/**/*"]
}
```

```text
"test": "cross-env TZ=Pacific/Auckland npm run test-tz
       && cross-env TZ=Europe/London npm run test-tz
       && cross-env TZ=America/Whitehorse npm run test-tz
       && npm run test-tz
       && jest --coverage --coverageThreshold=\"{ global: { lines: 100 } }\""
```

### Test organization

| Suite | Coverage |
|-------|----------|
| `test/constructor`, `test/parse` | Object creation and parsing |
| `test/get-set`, `test/manipulate` | Accessors and manipulation |
| `test/display`, `test/comparison`, `test/query` | Formatting and comparisons |
| `test/locale` + `test/locale/*` | Locale behavior (per-locale files) |
| `test/timezone` | Time-zone math (run under multiple `TZ` values) |
| `test/plugin/*` | One file per plugin (39 plugin test files, including `declarations.test.js` and `utc-utcOffset.test.js`) |
| `test/issues/*` | Regression tests pinned to specific GitHub issues |
| `test/utils` | Utility functions |
| `test/__mocks__/dayjs.js` | Jest mock for the library |

### Cross-timezone discipline

> [!IMPORTANT]
> The full test command runs the timezone suite four times under different `TZ` environment variables (Pacific/Auckland, Europe/London, America/Whitehorse, and the host default). Date libraries are notoriously sensitive to the host zone and DST rules; this matrix is how Day.js catches off-by-one and DST regressions before release. Run `npm test` (not just `jest`) locally so you exercise the same matrix.

### MockDate and moment parity

Tests use `mockdate` to freeze "now" deterministically, and import `moment`/`moment-timezone` as oracles to assert Moment.js-compatible output. When changing a method, the relevant moment-parity assertions are the bar to clear.

---

## 13. Security Analysis

Day.js is a pure computation library with a small and well-bounded attack surface.

| Area | Assessment |
|------|------------|
| **Network** | None. Day.js makes no network calls. |
| **Filesystem** | None at runtime. |
| **`eval` / dynamic code** | None. No `eval`, `Function(string)`, or dynamic `require`. |
| **Runtime dependencies** | **Zero.** No transitive supply-chain surface in production. |
| **Secrets / auth** | Not applicable; it is a client-side date utility. |
| **Native API reliance** | Built on `Date` and (for `timezone`) `Intl.DateTimeFormat`, both standard. |

### Regular-expression exposure (ReDoS)

The parse and format regexes (`REGEX_PARSE`, `REGEX_FORMAT`, the `customParseFormat` `formattingTokens`, and the `duration` ISO regex) are applied to:

- **Date input** — the value a user might control. `REGEX_PARSE` is anchored and bounded (fixed digit groups), so catastrophic backtracking is not expected on date strings.
- **Format strings** — generally developer-authored constants, not user input.

> [!CAUTION]
> Treat **format strings** as code, not data. Do not build a `format()` or `customParseFormat` template from untrusted input — while the regexes themselves are bounded, passing attacker-controlled format templates is an unnecessary risk and can leak unexpected output. User input belongs in the **date value** argument, validated with strict-mode `customParseFormat` + `isValid()`.

### Object-merge surfaces

`updateLocale` and `objectSupport` merge caller-supplied objects into internal structures. `updateLocale` spreads `customConfig` into the shared locale object.

> [!WARNING]
> Do not feed untrusted JSON directly into `dayjs.updateLocale()` or as object input to the factory. Although these paths operate on locale/date fields rather than arbitrary prototypes, supplying unvalidated third-party objects to configuration APIs is a general prototype-pollution hygiene concern. Keep locale configuration developer-controlled.

### Operational guidance

- Pin the Day.js version and rely on the lockfile; with zero runtime deps, an audit of Day.js is an audit of one package.
- Keep `devHelper` out of production builds (it is auto-suppressed when `NODE_ENV === 'production'`, but excluding it entirely is cleaner).
- Validate all user-entered dates with strict parsing before use.

---

## 14. Code Quality and Patterns

### Recurring patterns

- **Decorator method-wrapping** — `const old = proto.x; proto.x = function(...) { … old.bind(this)(...) }`. The backbone of the plugin system; used consistently across parse/format/manipulate extensions.
- **Single-factory immutability** — all new instances flow through `wrapper()`, guaranteeing locale/UTC/offset propagation in one place.
- **Unit normalization** — every unit argument passes through `prettyUnit` (`Utils.p`), so long, short, and plural forms converge to one canonical constant before branching.
- **Generated accessors** — unit getters/setters are produced from a data table rather than written out, reducing surface area and size.
- **Size-driven minimalism** — one-letter utility keys, no helper libraries, English-only core. Readability is traded for bytes deliberately and consistently.
- **Moment parity by porting** — `monthDiff` is copied from Moment.js specifically to reproduce its results; comparisons in tests assert against moment.

### Conventions

- **ESLint airbnb-base** with targeted `eslint-disable` comments where the size/parity goals conflict with default rules (e.g. `no-use-before-define` for the mutually recursive `isDayjs`/`Dayjs`).
- **Zero-indexed months** throughout, matching native `Date`.
- **`$`-prefixed internal fields** (`$d`, `$y`, `$L`, `$u`, `$x`, …) to signal "private, do not depend on."

### Complexity hotspots

| File | Note |
|------|------|
| `src/plugin/customParseFormat/index.js` (~7.7&nbsp;KB) | The largest plugin: a token table, a hand-rolled parser, strict-mode validation, and format-array fallthrough. The densest logic in the codebase. |
| `src/plugin/duration/index.js` (~8&nbsp;KB) | A second class (`Duration`) with its own parse/format/arithmetic surface and ISO 8601 duration handling. |
| `src/plugin/timezone/index.js` (~5&nbsp;KB) | DST-aware offset resolution via `Intl`, including the `fixOffset` "hole/overlap" handling adapted from Luxon. |
| `src/index.js` `startOf`/`$set` | Branch-heavy unit switches; correct but worth reading carefully before editing. |

---

## 15. Internal Field Reference

These `$`-prefixed fields appear throughout the source. They are implementation detail — do not depend on them from application code — but you must understand them to read or modify the library.

| Field | Meaning |
|-------|---------|
| `$d` | The wrapped native `Date` |
| `$L` | Active locale name for this instance |
| `$y` | Cached full year |
| `$M` | Cached month (0–11) |
| `$D` | Cached date of month (1–31) |
| `$W` | Cached day of week (0–6) |
| `$H` | Cached hour |
| `$m` | Cached minute |
| `$s` | Cached second |
| `$ms` | Cached millisecond |
| `$u` | UTC-mode flag (set by `utc` plugin) |
| `$x` | Extension data bag (e.g. `$x.$timezone`, `$x.$localOffset`) |
| `$offset` | Fixed offset in minutes (utc/timezone plugins) |
| `$isDayjsObject` | Marker used by `isDayjs` for cross-realm detection |
| `$i` | (On a plugin function) "already installed" guard |

Shared `Utils`/`$utils()` keys: `s` padStart, `z` padZoneStr, `m` monthDiff, `a` absFloor, `p` prettyUnit, `u` isUndefined, `l` parseLocale, `i` isDayjs, `w` wrapper.

Factory statics: `dayjs.Ls` (locale registry), `dayjs.en` (English locale), `dayjs.p` (plugin flag registry), `dayjs.extend`, `dayjs.locale`, `dayjs.isDayjs`, `dayjs.unix`.

---

## 16. Potential Improvements and Tech Debt

These observations come directly from in-source markers and the project's tracked issues.

### In-code markers

- **`$offset` coupling** — `wrapper()` carries `$offset` with the comment `todo: refactor; do not use this.$offset in you code`. The UTC/timezone offset handling is threaded through this shared field in a way the author flagged as needing cleanup.
- **`customParseFormat` meridiem matching** — a `todo: fix input === meridiem(i, 0, isLowerCase)` marks an imprecise meridiem comparison loop that scans 1–24 hours.

### Tracked issues (from the bundled issue snapshot)

| Issue | Status | Area |
|-------|--------|------|
| #1765 — Day.js does not actually support Node.js ESM | Open | Module/packaging |
| #1437 — Timezone `startOf` DST off-by-one | Open | `timezone` |
| #3048 / #3049 / #3054 — `weekOfYear` week 53 for `de-at` / `de-ch` / `nl-be` | Open | `weekOfYear` + locale |
| #2970 — Disable rounding in `duration().humanize()` | Open | `duration` |
| #2888 — `dayjs(undefined)` vs `dayjs()` semantics | Closed | Parsing |

### Architectural suggestions

- **ESM packaging** (#1765) is the most consequential open item — proper `exports`/conditional-exports support would resolve a long-standing interop pain point.
- **Locale-aware week numbering** — the cluster of week-53 issues suggests `weekOfYear`/`weekYear` could use a more rigorous ISO/locale week algorithm, or clearer documentation of the `yearStart`/`weekStart` interaction.
- **Configurable duration rounding** (#2970) — exposing the rounding function on `humanize()` (as `relativeTime` already does via its `rounding` option) would be a small, consistent addition.
- **DST edge cases** in `timezone` (#1437) remain the perennial hard problem; the `fixOffset` logic is sound but boundary handling around DST transitions warrants targeted tests.

> [!NOTE]
> None of these are correctness problems in common use. They are edge cases (DST boundaries, specific regional week rules), ergonomics (ESM, rounding options), and one flagged internal refactor. The 100% coverage gate and multi-zone test matrix keep the mainline behavior dependable.

---

## 17. Contributing

### Local setup

```bash
git clone https://github.com/iamkun/dayjs.git
cd dayjs
npm install
```

### Development loop

```bash
# Lint (also runs on pre-commit)
npm run lint

# Full test matrix (multi-timezone + 100% coverage gate)
npm test

# Build and verify the size budget
npm run build
```

> [!IMPORTANT]
> Run the **full** `npm test`, not a bare `jest` invocation — the multi-`TZ` matrix and the 100% line-coverage threshold are part of the contract. A change that drops coverage below 100% or breaks under a non-host timezone will fail CI.

### Adding a plugin

1. Create `src/plugin/<name>/index.js` exporting `(option, Dayjs, dayjs) => { … }`.
2. Patch `Dayjs.prototype` and/or add `dayjs.*` statics, always delegating to saved originals when wrapping.
3. Add `types/plugin/<name>.d.ts` that `declare module 'dayjs'` to augment the `Dayjs` interface or factory.
4. Add `test/plugin/<name>.test.js` with full coverage (including a `declarations.test.js` type check where relevant).
5. Document the new methods in `docs/`.

### Adding a locale

1. Create `src/locale/<name>.js` exporting a locale object (`name`, `weekdays`, `months`, `ordinal`, and any of `weekStart`, `yearStart`, `formats`, `relativeTime`, `meridiem`, `calendar`).
2. Self-register on import as the existing locales do.
3. Add `test/locale/<name>.test.js`.

### Commit conventions

Commits drive **semantic-release**, so follow the conventional-commit format (`feat:`, `fix:`, `docs:`, `chore:`, …). The version, changelog, and GitHub release are generated from commit history — do not hand-edit the version in `package.json` (it stays `0.0.0-development`).

### Code style

- Conform to `eslint-config-airbnb-base`; justify any `eslint-disable` with a comment.
- Preserve immutability — route new instances through `wrapper()`; never mutate `this` in core methods.
- Mind the size budget: prefer reusing `Utils` helpers over adding code to the core.

---

## 18. Quick Reference

### File map

```
src/
  constant.js              # units, regexes, FORMAT_DEFAULT
  utils.js                 # s,z,m,a,p,u helper object
  index.js                 # dayjs() factory + Dayjs class + locale registry
  locale/*.js              # 143 locales (en built into core)
  plugin/*/index.js        # 37 plugins
types/
  index.d.ts               # core types + PluginFunc
  plugin/*.d.ts            # per-plugin augmentation
test/
  *.test.js                # core suites
  plugin/*.test.js         # per-plugin suites
  locale/*.test.js         # per-locale suites
  issues/*.test.js         # regression tests
```

### Key npm scripts

| Script | Effect |
|--------|--------|
| `npm test` | Multi-TZ Jest + 100% coverage gate |
| `npm run build` | Bundle `dayjs.min.js` + size check |
| `npm run babel` | Build ESM under `esm/` |
| `npm run lint` | ESLint over `src`, `test`, `build` |
| `npm run size` | Enforce 2.99&nbsp;KB budget |

### Plugin authoring contract

```js
// src/plugin/<name>/index.js
export default (option, Dayjs, dayjs) => {
  const proto = Dayjs.prototype
  const old = proto.someMethod
  proto.someMethod = function (...args) {
    // new behavior, then delegate
    return old.bind(this)(...args)
  }
  dayjs.someStatic = function () { /* … */ }
}
```

### Hard plugin dependencies

| Plugin | Extend first |
|--------|--------------|
| `timezone` | `utc` |
| `weekYear` | `weekOfYear` |
| `isoWeeksInYear` | `isLeapYear` |
| `duration` (`humanize`) | `relativeTime` |

### Internal invariants

- New instances are produced only by `wrapper()` (except in `badMutable`).
- All units normalize through `prettyUnit` before branching.
- The core imports no plugin and no non-English locale.
- `dayjs.min.js` must stay ≤ 2.99&nbsp;KB.

---

_Generated June 2026 from iamkun/dayjs at 6609c5e. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
