diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 53fcad0..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,36 +0,0 @@ -module.exports = { - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint'], - extends: [ - 'eslint:recommended', - 'airbnb-base', - "plugin:@typescript-eslint/eslint-recommended", - 'plugin:@typescript-eslint/recommended', - ], - rules: { - 'import/extensions': ['error', { - 'ts': 'never', - 'json': 'always' - }], - 'class-methods-use-this': 'off', - 'no-restricted-syntax': 'off', - 'no-param-reassign': 'off', - 'no-cond-assign': 'off', - 'no-useless-escape': 'off', - 'quotes': 'error', - 'semi': 'error', - 'comma-dangle': ['error', { - 'arrays': 'always', - }], - }, - env: { - browser: true, - }, - settings: { - 'import/resolver': 'webpack', - }, - parserOptions: { - ecmaVersion: 2018, - sourceType: 'module', - }, -}; diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4fc309a..d4eef7d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,17 +5,25 @@ on: tags: - 'v*.*.*' +permissions: + id-token: write # Required for OIDC + contents: read + jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - uses: actions/setup-node@v1 + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 with: - node-version: 12 + node-version: 24.x registry-url: https://registry.npmjs.org/ - run: npm install - - run: npm version "${GITHUB_REF:11}" --git-tag-version false --commit-hooks false - - run: npm publish --access public - env: - NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + - run: | + VERSION="${GITHUB_REF:11}" + npm version "$VERSION" --git-tag-version false --commit-hooks false + if [[ "$VERSION" == *"-"* ]]; then + npm publish --access public --tag next + else + npm publish --access public + fi diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index c547248..568444f 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -3,15 +3,17 @@ on: branches: - '*' # matches every branch - '*/*' # matches every branch containing a single '/' + pull_request: jobs: - eslint: - name: eslint + quality: + name: quality runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v6 - name: Use Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v6 with: - node-version: 12.x + node-version: 24.x - run: npm install - run: npm run eslint + - run: npm test diff --git a/README.md b/README.md index c1d44b1..87b92e7 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,105 @@ -# Date Formatter +# @date-js/date-formatter -- A light javascript tool for formatting date (<3kB). -- Zero dependency. -- TypeScript compatible. +[![npm](https://img.shields.io/npm/v/@date-js/date-formatter)](https://www.npmjs.com/package/@date-js/date-formatter) +[![license](https://img.shields.io/npm/l/@date-js/date-formatter)](LICENSE) -## Example +**Format a date. Nothing else. Under 1KB gzipped.** -``` javascript -DateFormatter.format('%l %j %F %Y', new Date()); -// Sunday 12 October 2014 +No config. No plugins. No dependencies. Locale support out of the box via the native `Intl` API. + +```javascript +DateFormatter.format('%l, %F %j %Y', new Date()); +// Sunday, March 29 2026 + +DateFormatter.format('%l %j %F %Y', new Date(), 'fr-FR'); +// dimanche 29 mars 2026 ``` -``` javascript -DateFormatter.format('%l %j %F %Y', new Date(), 'fr_FR'); -// Samedi 12 octobre 2014 +## Why this lib? + +If all you need is to display a date, there's no point in shipping a full date toolkit. Perfect for landing pages, lightweight widgets, or any project where pulling in a full date library would be overkill. + +- **Under 1KB gzipped** — no dead code, no bloat +- **Zero dependencies** — nothing to audit, nothing to break +- **Locale support out of the box** — automatically uses the browser language, no config needed +- **Familiar syntax** — `%Y-%m-%d` style, like PHP's `date()` or Python's `strftime` + +## Install & Usage + +### With a bundler (webpack, Vite, Rollup…) + +```bash +npm install @date-js/date-formatter ``` -By default, Date Formatter uses the current browser language. You can force a locale like this: -``` javascript +Use the default export to get a ready-to-use singleton: + +```javascript +import DateFormatter from '@date-js/date-formatter'; + +DateFormatter.format('%d/%m/%Y', new Date()); // 29/03/2026 DateFormatter.setLocale('fr-FR'); +DateFormatter.format('%l %j %F %Y', new Date()); // dimanche 29 mars 2026 ``` -You can see examples in example directory. +Or import the class directly to create isolated instances (useful when managing multiple locales): -## Install it +```javascript +import { DateFormatter } from '@date-js/date-formatter'; -### With NPM +const fr = new DateFormatter('fr-FR'); +const en = new DateFormatter('en-US'); -``` bash -npm install @date-js/date-formatter --save +fr.format('%l %j %F %Y', new Date()); // dimanche 29 mars 2026 +en.format('%l %j %F %Y', new Date()); // Sunday March 29 2026 ``` -``` javascript -import DateFormatter from '@date-js/date-formatter'; +### In a browser — classic script tag + +The UMD bundle exposes `DateFormatter` as a global: + +```html + + ``` -### Otherwise +### In a browser — ES module + +Modern browsers support ` + DateFormatter.format('%d/%m/%Y', new Date()); // 29/03/2026 + ``` -## Format - -| Character | Description | Example | -| ------------- |:-------------:| -----:| -| %Y | A full numeric representation of a year, 4 digits | 2020 | -| %y | A two digit representation of a year | 20 | -| %d | Day of the month, 2 digits with leading zeros | 01 to 31 | -| %l | A full textual representation of the day of the week | Sunday through Saturday | -| %D | A textual representation of a day, three letters | Mon through Sun | -| %F | A full textual representation of a month, such as January or March | January through December | -| %M | A short textual representation of a month, three letters | Jan through Dec | -| %m | Numeric representation of a month, with leading zeros | 01 through 12 | -| %n | Numeric representation of a month, without leading zeros | 1 through 12 | -| %G | 24-hour format of an hour without leading zeros | 0 through 23 | -| %H | 24-hour format of an hour with leading zeros | 00 through 23 | -| %i | Minutes with leading zeros | 00 to 59 | -| %s | Seconds with leading zeros | 00 to 59 | +## Format specifiers + +| Specifier | Description | Example | +|---|---|---| +| `%Y` | Year, 4 digits | `2026` | +| `%y` | Year, 2 digits | `26` | +| `%d` | Day of the month, with leading zero | `01` – `31` | +| `%j` | Day of the month, without leading zero | `1` – `31` | +| `%l` | Full weekday name *(locale-aware)* | `Sunday`, `dimanche` | +| `%D` | Short weekday name *(locale-aware)* | `Sun`, `dim.` | +| `%F` | Full month name *(locale-aware)* | `March`, `mars` | +| `%M` | Short month name *(locale-aware)* | `Mar`, `mars` | +| `%m` | Month, with leading zero | `01` – `12` | +| `%n` | Month, without leading zero | `1` – `12` | +| `%H` | Hour (24h), with leading zero | `00` – `23` | +| `%G` | Hour (24h), without leading zero | `0` – `23` | +| `%i` | Minutes, with leading zero | `00` – `59` | +| `%s` | Seconds, with leading zero | `00` – `59` | + +Escape a `%` with a backslash: `\%`. + +## License + +MIT diff --git a/bin/docker.sh b/bin/docker.sh new file mode 100755 index 0000000..8b42af8 --- /dev/null +++ b/bin/docker.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +DIRECTORY=$(dirname $(realpath $0 )) + +TTY_FLAG=$([ -t 0 ] && echo "-it" || echo "-i") +docker run $TTY_FLAG --rm \ + -v "$DIRECTORY/..":/home/node/app \ + -w /home/node/app \ + -u $(id -u ${USER}):$(id -g ${USER}) \ + node:24-slim \ + "${@:-bash}" diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..44f67b5 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,12 @@ +const tseslint = require('typescript-eslint'); + +module.exports = tseslint.config( + ...tseslint.configs.recommended, + { + files: ['**/*.ts'], + rules: { + 'quotes': ['error', 'single'], + 'semi': 'error', + }, + } +); diff --git a/package.json b/package.json index 8ac961d..b99111f 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,22 @@ { "name": "@date-js/date-formatter", "version": "0.0.0-version", - "description": "A light javascript tool for formatting date.", + "description": "Tiny date formatter with locale support. Zero dependencies, under 1KB gzipped.", "main": "dist/date-formatter.js", + "exports": { + ".": { + "import": "./dist/date-formatter.js", + "require": "./dist/date-formatter.js", + "types": "./dist/types/index.d.ts" + } + }, "scripts": { "prepublishOnly": "webpack", - "eslint": "eslint ./src --ext .ts,.js,.d.ts", - "eslint-fix": "eslint ./src --ext .ts,.js,.d.ts --fix", + "eslint": "eslint src", + "eslint-fix": "eslint src --fix", "watch": "webpack --watch", - "build": "webpack" + "build": "webpack", + "test": "vitest run" }, "repository": { "type": "git", @@ -16,11 +24,18 @@ }, "keywords": [ "date", - "light", - "small", - "tiny", "format", - "formatter" + "formatter", + "date-format", + "strftime", + "intl", + "locale", + "i18n", + "lightweight", + "tiny", + "zero-dependency", + "browser", + "typescript" ], "files": [ "dist" @@ -33,30 +48,12 @@ }, "homepage": "https://github.com/date-js/date-formatter#readme", "devDependencies": { - "@babel/core": "^7.14.6", - "@babel/preset-env": "^7.14.7", - "@babel/preset-typescript": "^7.14.5", - "@types/node": "^12.12.62", - "@typescript-eslint/eslint-plugin": "^2.34.0", - "@typescript-eslint/parser": "^2.34.0", - "babel-loader": "^8.2.2", - "eslint": "^6.8.0", - "eslint-config-airbnb-base": "^14.2.0", - "eslint-config-standard": "^14.1.1", - "eslint-import-resolver-webpack": "^0.12.2", - "eslint-plugin-import": "^2.22.1", - "eslint-plugin-promise": "^4.3.1", - "path": "^0.12.7", - "terser-webpack-plugin": "^5.1.4", - "ts-loader": "^9.2.3", - "typescript": "^4.3.4", - "webpack": "^5.41.0", - "webpack-cli": "^4.7.2" - }, - "babel": { - "presets": [ - "@babel/preset-env", - "@babel/preset-typescript" - ] + "eslint": "^10.1.0", + "ts-loader": "^9.5.4", + "typescript": "^5.9.3", + "typescript-eslint": "^8.57.2", + "vitest": "^4.1.2", + "webpack": "^5.105.4", + "webpack-cli": "^7.0.2" } } diff --git a/src/App.ts b/src/DateFormatter.ts similarity index 52% rename from src/App.ts rename to src/DateFormatter.ts index 206da56..f5d551d 100644 --- a/src/App.ts +++ b/src/DateFormatter.ts @@ -1,5 +1,9 @@ -export default class App { - protected locale: string = navigator.language; +export default class DateFormatter { + private locale: string; + + constructor(locale?: string) { + this.locale = locale ?? (typeof navigator !== 'undefined' ? navigator.language : 'en'); + } public setLocale(locale: string): void { this.locale = locale; @@ -8,27 +12,30 @@ export default class App { public format(format: string, date: Date, locale?: string): string { let formatted = format; let varData; - const varRegexp = /%([a-z]+)/gi; + let delta = 0; + const varRegexp = /(? { + it('%Y — full year', () => { + expect(DateFormatter.format('%Y', date)).toBe('2025'); + }); + + it('%y — 2-digit year', () => { + expect(DateFormatter.format('%y', date)).toBe('25'); + }); + + it('%d — day with zero padding', () => { + expect(DateFormatter.format('%d', date)).toBe('05'); + }); + + it('%j — day without zero padding', () => { + expect(DateFormatter.format('%j', date)).toBe('5'); + }); + + it('%m — month with zero padding', () => { + expect(DateFormatter.format('%m', date)).toBe('01'); + }); + + it('%n — month without zero padding', () => { + expect(DateFormatter.format('%n', date)).toBe('1'); + }); + + it('%H — hours with zero padding', () => { + expect(DateFormatter.format('%H', date)).toBe('09'); + }); + + it('%G — hours without zero padding', () => { + expect(DateFormatter.format('%G', date)).toBe('9'); + }); + + it('%i — minutes with zero padding', () => { + expect(DateFormatter.format('%i', date)).toBe('03'); + }); + + it('%s — seconds with zero padding', () => { + expect(DateFormatter.format('%s', date)).toBe('07'); + }); + + it('%l — full weekday name (en)', () => { + expect(DateFormatter.format('%l', date, 'en')).toBe('Sunday'); + }); + + it('%D — short weekday name (en)', () => { + expect(DateFormatter.format('%D', date, 'en')).toBe('Sun'); + }); + + it('%F — full month name (en)', () => { + expect(DateFormatter.format('%F', date, 'en')).toBe('January'); + }); + + it('%M — short month name (en)', () => { + expect(DateFormatter.format('%M', date, 'en')).toBe('Jan'); + }); +}); + +describe('combined formats', () => { + it('ISO-like date %Y-%m-%d', () => { + expect(DateFormatter.format('%Y-%m-%d', date)).toBe('2025-01-05'); + }); + + it('full datetime %Y-%m-%d %H:%i:%s', () => { + expect(DateFormatter.format('%Y-%m-%d %H:%i:%s', date)).toBe('2025-01-05 09:03:07'); + }); +}); + +describe('percent escaping', () => { + it('\\% outputs a literal %', () => { + expect(DateFormatter.format('\\%Y', date)).toBe('%Y'); + }); + + it('mixed escaped and unescaped', () => { + expect(DateFormatter.format('\\%Y %Y', date)).toBe('%Y 2025'); + }); +}); + +describe('locale', () => { + it('locale passed to format() is used', () => { + expect(DateFormatter.format('%l', date, 'fr-FR')).toBe('dimanche'); + }); + + it('setLocale() changes the default locale', () => { + const instance = new DateFormatterClass('en'); + instance.setLocale('fr-FR'); + expect(instance.format('%l', date)).toBe('dimanche'); + }); + + it('locale passed to format() takes precedence over setLocale()', () => { + const instance = new DateFormatterClass('fr-FR'); + expect(instance.format('%l', date, 'en')).toBe('Sunday'); + }); +}); + +describe('unknown specifier', () => { + it('unknown specifier is left as-is', () => { + expect(DateFormatter.format('%z', date)).toBe('%z'); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index ee5ce83..c977829 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,8 +4,6 @@ "outDir": "./dist/", "declarationDir": "./dist/types", "declaration": true, - "noImplicitAny": true, - "resolveJsonModule": true, "esModuleInterop": true, "lib": [ "es2015", diff --git a/webpack.config.js b/webpack.config.js index 611ff04..02a176c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,40 +1,25 @@ const path = require('path'); -const TerserPlugin = require('terser-webpack-plugin'); -const config = { +module.exports = { mode: 'production', - entry: { - 'index': './src/index.ts', - }, + entry: './src/index.ts', output: { filename: 'date-formatter.js', path: path.resolve(__dirname, 'dist'), libraryTarget: 'umd', - library: 'DateFormatter' + library: 'DateFormatter', + libraryExport: 'default', }, resolve: { - extensions: ['.ts', '.js', '.json'], + extensions: ['.ts', '.js'], }, module: { rules: [ - { - test: /\.ts$/, - exclude: /(node_modules)/, - use: { - loader: 'babel-loader', - } - }, { test: /\.ts$/, use: 'ts-loader', exclude: /node_modules/, - } - ] - }, - optimization: { - minimize: true, - minimizer: [new TerserPlugin()], + }, + ], }, }; - -module.exports = [config];