refactor: code quality improvements across the board

- Add input validation (TypeError) to cycleMonth and cycleYear
- Convert tests to node:test runner with describe/it structure
- Add ESLint + Prettier with lint, format, and format:check scripts
- Update CI workflow with lint job, frozen-lockfile, and pack-check
- Add noImplicitReturns, noFallthroughCasesInSwitch, skipLibCheck to tsconfig
- Add invalid Date tests for both ESM and CJS suites
This commit is contained in:
Aric Camarata 2026-03-08 11:32:47 -04:00
parent 24db90e9f5
commit 9b9abb99c8
12 changed files with 1050 additions and 310 deletions

View file

@ -8,95 +8,76 @@ on:
jobs:
test:
name: Test (Node ${{ matrix.node-version }})
name: Test (Node ${{ matrix.node }})
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22, 24]
node: [20, 22, 24]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
node-version: ${{ matrix.node }}
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: node --test test.mjs
- run: node --test test-cjs.cjs
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm build
- name: Test (ESM)
run: node test.mjs
- name: Test (CJS)
run: node test-cjs.cjs
lint:
name: Lint & Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run lint
- run: pnpm run format:check
typecheck:
name: Type Check
name: Typecheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js 24
uses: actions/setup-node@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Type check
run: pnpm typecheck
- run: pnpm install --frozen-lockfile
- run: pnpm run typecheck
pack-check:
name: Pack Check
name: Pack check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js 24
uses: actions/setup-node@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm build
- name: Check pack contents
- run: pnpm install --frozen-lockfile
- run: pnpm build
- name: Verify pack contents
run: |
npm pack --dry-run 2>&1 | tee pack-output.txt
# Verify expected files are present
grep -q "dist/index.cjs" pack-output.txt
grep -q "dist/index.mjs" pack-output.txt
grep -q "dist/index.d.ts" pack-output.txt
grep -q "dist/index.d.mts" pack-output.txt
grep -q "README.md" pack-output.txt
grep -q "LICENSE" pack-output.txt
grep -q "CHANGELOG.md" pack-output.txt
# Verify image folders are NOT in the pack
! grep -q "mm-256" pack-output.txt
! grep -q "my-256" pack-output.txt
PACK=$(npm pack --dry-run 2>&1)
echo "$PACK"
for f in dist/index.cjs dist/index.mjs dist/index.d.ts dist/index.d.mts README.md LICENSE CHANGELOG.md; do
echo "$PACK" | grep -q "$f" || (echo "Missing: $f" && exit 1)
done
echo "$PACK" | grep -q "src/" && (echo "ERROR: src/ should not be in pack" && exit 1) || true
echo "Pack check passed"

6
.gitignore vendored
View file

@ -56,3 +56,9 @@ coverage/
.windsurf/
.cody/
.sourcegraph/
.vscode/*
.codex/
.aider/
.aider.chat.history.md
.continue/
.gemini/

6
.prettierrc Normal file
View file

@ -0,0 +1,6 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2
}

12
eslint.config.mjs Normal file
View file

@ -0,0 +1,12 @@
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import prettier from 'eslint-config-prettier';
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
prettier,
{
ignores: ['dist/', 'node_modules/', 'test.mjs', 'test-cjs.cjs'],
},
);

View file

@ -29,8 +29,11 @@
"scripts": {
"build": "tsup",
"typecheck": "tsc --noEmit",
"lint": "eslint src/",
"format": "prettier --write src/ test.mjs test-cjs.cjs eslint.config.mjs tsup.config.ts",
"format:check": "prettier --check src/ test.mjs test-cjs.cjs eslint.config.mjs tsup.config.ts",
"pretest": "tsup",
"test": "node test.mjs && node test-cjs.cjs",
"test": "node --test test.mjs && node --test test-cjs.cjs",
"prepublishOnly": "tsup"
},
"engines": {
@ -62,8 +65,13 @@
"registry": "https://registry.npmjs.org/"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/node": "^22.0.0",
"eslint": "^10.0.3",
"eslint-config-prettier": "^10.1.8",
"prettier": "^3.8.1",
"tsup": "^8.0.0",
"typescript": "^5.0.0"
"typescript": "^5.0.0",
"typescript-eslint": "^8.56.1"
}
}

View file

@ -8,15 +8,30 @@ importers:
.:
devDependencies:
'@eslint/js':
specifier: ^10.0.1
version: 10.0.1(eslint@10.0.3)
'@types/node':
specifier: ^22.0.0
version: 22.19.11
eslint:
specifier: ^10.0.3
version: 10.0.3
eslint-config-prettier:
specifier: ^10.1.8
version: 10.1.8(eslint@10.0.3)
prettier:
specifier: ^3.8.1
version: 3.8.1
tsup:
specifier: ^8.0.0
version: 8.5.1(typescript@5.9.3)
typescript:
specifier: ^5.0.0
version: 5.9.3
typescript-eslint:
specifier: ^8.56.1
version: 8.56.1(eslint@10.0.3)(typescript@5.9.3)
packages:
@ -176,6 +191,61 @@ packages:
cpu: [x64]
os: [win32]
'@eslint-community/eslint-utils@4.9.1':
resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
'@eslint-community/regexpp@4.12.2':
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
'@eslint/config-array@0.23.3':
resolution: {integrity: sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint/config-helpers@0.5.3':
resolution: {integrity: sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint/core@1.1.1':
resolution: {integrity: sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint/js@10.0.1':
resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
peerDependencies:
eslint: ^10.0.0
peerDependenciesMeta:
eslint:
optional: true
'@eslint/object-schema@3.0.3':
resolution: {integrity: sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint/plugin-kit@0.6.1':
resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
'@humanfs/node@0.16.7':
resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==}
engines: {node: '>=18.18.0'}
'@humanwhocodes/module-importer@1.0.1':
resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
engines: {node: '>=12.22'}
'@humanwhocodes/retry@0.4.3':
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'}
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@ -327,20 +397,101 @@ packages:
cpu: [x64]
os: [win32]
'@types/esrecurse@4.3.1':
resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==}
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/node@22.19.11':
resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==}
'@typescript-eslint/eslint-plugin@8.56.1':
resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
'@typescript-eslint/parser': ^8.56.1
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/parser@8.56.1':
resolution: {integrity: sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/project-service@8.56.1':
resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/scope-manager@8.56.1':
resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/tsconfig-utils@8.56.1':
resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/type-utils@8.56.1':
resolution: {integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/types@8.56.1':
resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.56.1':
resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/utils@8.56.1':
resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/visitor-keys@8.56.1':
resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
acorn@8.16.0:
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
engines: {node: '>=0.4.0'}
hasBin: true
ajv@6.14.0:
resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
balanced-match@4.0.4:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
brace-expansion@5.0.4:
resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==}
engines: {node: 18 || 20 || >=22}
bundle-require@5.1.0:
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@ -366,6 +517,10 @@ packages:
resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==}
engines: {node: ^14.18.0 || >=16.10.0}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
@ -375,11 +530,75 @@ packages:
supports-color:
optional: true
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
esbuild@0.27.3:
resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
engines: {node: '>=18'}
hasBin: true
escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
eslint-config-prettier@10.1.8:
resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==}
hasBin: true
peerDependencies:
eslint: '>=7.0.0'
eslint-scope@9.1.2:
resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
eslint-visitor-keys@3.4.3:
resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
eslint-visitor-keys@5.0.1:
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
eslint@10.0.3:
resolution: {integrity: sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
hasBin: true
peerDependencies:
jiti: '*'
peerDependenciesMeta:
jiti:
optional: true
espree@11.2.0:
resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
esquery@1.7.0:
resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
engines: {node: '>=0.10'}
esrecurse@4.3.0:
resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
engines: {node: '>=4.0'}
estraverse@5.3.0:
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
engines: {node: '>=4.0'}
esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fast-json-stable-stringify@2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
@ -389,18 +608,76 @@ packages:
picomatch:
optional: true
file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
find-up@5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
fix-dts-default-cjs-exports@1.0.1:
resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==}
flat-cache@4.0.1:
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
engines: {node: '>=16'}
flatted@3.3.4:
resolution: {integrity: sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
glob-parent@6.0.2:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
ignore@7.0.5:
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
engines: {node: '>= 4'}
imurmurhash@0.1.4:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
joycon@3.1.1:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
engines: {node: '>=10'}
json-buffer@3.0.1:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
levn@0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
lilconfig@3.1.3:
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
engines: {node: '>=14'}
@ -412,9 +689,17 @@ packages:
resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
minimatch@10.2.4:
resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
engines: {node: 18 || 20 || >=22}
mlly@1.8.0:
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
@ -424,10 +709,33 @@ packages:
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
p-locate@5.0.0:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
@ -463,6 +771,19 @@ packages:
yaml:
optional: true
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
prettier@3.8.1:
resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==}
engines: {node: '>=14'}
hasBin: true
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
readdirp@4.1.2:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
@ -476,6 +797,19 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
semver@7.7.4:
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
engines: {node: '>=10'}
hasBin: true
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
shebang-regex@3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
source-map@0.7.6:
resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
engines: {node: '>= 12'}
@ -503,6 +837,12 @@ packages:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
ts-api-utils@2.4.0:
resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==}
engines: {node: '>=18.12'}
peerDependencies:
typescript: '>=4.8.4'
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
@ -525,6 +865,17 @@ packages:
typescript:
optional: true
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
typescript-eslint@8.56.1:
resolution: {integrity: sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.0.0'
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
@ -536,6 +887,22 @@ packages:
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
hasBin: true
word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
snapshots:
'@esbuild/aix-ppc64@0.27.3':
@ -616,6 +983,51 @@ snapshots:
'@esbuild/win32-x64@0.27.3':
optional: true
'@eslint-community/eslint-utils@4.9.1(eslint@10.0.3)':
dependencies:
eslint: 10.0.3
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {}
'@eslint/config-array@0.23.3':
dependencies:
'@eslint/object-schema': 3.0.3
debug: 4.4.3
minimatch: 10.2.4
transitivePeerDependencies:
- supports-color
'@eslint/config-helpers@0.5.3':
dependencies:
'@eslint/core': 1.1.1
'@eslint/core@1.1.1':
dependencies:
'@types/json-schema': 7.0.15
'@eslint/js@10.0.1(eslint@10.0.3)':
optionalDependencies:
eslint: 10.0.3
'@eslint/object-schema@3.0.3': {}
'@eslint/plugin-kit@0.6.1':
dependencies:
'@eslint/core': 1.1.1
levn: 0.4.1
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.7':
dependencies:
'@humanfs/core': 0.19.1
'@humanwhocodes/retry': 0.4.3
'@humanwhocodes/module-importer@1.0.1': {}
'@humanwhocodes/retry@0.4.3': {}
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@ -705,16 +1117,128 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.59.0':
optional: true
'@types/esrecurse@4.3.1': {}
'@types/estree@1.0.8': {}
'@types/json-schema@7.0.15': {}
'@types/node@22.19.11':
dependencies:
undici-types: 6.21.0
'@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.3)(typescript@5.9.3))(eslint@10.0.3)(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
'@typescript-eslint/parser': 8.56.1(eslint@10.0.3)(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.56.1
'@typescript-eslint/type-utils': 8.56.1(eslint@10.0.3)(typescript@5.9.3)
'@typescript-eslint/utils': 8.56.1(eslint@10.0.3)(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.56.1
eslint: 10.0.3
ignore: 7.0.5
natural-compare: 1.4.0
ts-api-utils: 2.4.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.56.1(eslint@10.0.3)(typescript@5.9.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.56.1
'@typescript-eslint/types': 8.56.1
'@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.56.1
debug: 4.4.3
eslint: 10.0.3
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.56.1(typescript@5.9.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3)
'@typescript-eslint/types': 8.56.1
debug: 4.4.3
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/scope-manager@8.56.1':
dependencies:
'@typescript-eslint/types': 8.56.1
'@typescript-eslint/visitor-keys': 8.56.1
'@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
'@typescript-eslint/type-utils@8.56.1(eslint@10.0.3)(typescript@5.9.3)':
dependencies:
'@typescript-eslint/types': 8.56.1
'@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
'@typescript-eslint/utils': 8.56.1(eslint@10.0.3)(typescript@5.9.3)
debug: 4.4.3
eslint: 10.0.3
ts-api-utils: 2.4.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/types@8.56.1': {}
'@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)':
dependencies:
'@typescript-eslint/project-service': 8.56.1(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3)
'@typescript-eslint/types': 8.56.1
'@typescript-eslint/visitor-keys': 8.56.1
debug: 4.4.3
minimatch: 10.2.4
semver: 7.7.4
tinyglobby: 0.2.15
ts-api-utils: 2.4.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.56.1(eslint@10.0.3)(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3)
'@typescript-eslint/scope-manager': 8.56.1
'@typescript-eslint/types': 8.56.1
'@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
eslint: 10.0.3
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/visitor-keys@8.56.1':
dependencies:
'@typescript-eslint/types': 8.56.1
eslint-visitor-keys: 5.0.1
acorn-jsx@5.3.2(acorn@8.16.0):
dependencies:
acorn: 8.16.0
acorn@8.16.0: {}
ajv@6.14.0:
dependencies:
fast-deep-equal: 3.1.3
fast-json-stable-stringify: 2.1.0
json-schema-traverse: 0.4.1
uri-js: 4.4.1
any-promise@1.3.0: {}
balanced-match@4.0.4: {}
brace-expansion@5.0.4:
dependencies:
balanced-match: 4.0.4
bundle-require@5.1.0(esbuild@0.27.3):
dependencies:
esbuild: 0.27.3
@ -732,10 +1256,18 @@ snapshots:
consola@3.4.2: {}
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
debug@4.4.3:
dependencies:
ms: 2.1.3
deep-is@0.1.4: {}
esbuild@0.27.3:
optionalDependencies:
'@esbuild/aix-ppc64': 0.27.3
@ -765,31 +1297,164 @@ snapshots:
'@esbuild/win32-ia32': 0.27.3
'@esbuild/win32-x64': 0.27.3
escape-string-regexp@4.0.0: {}
eslint-config-prettier@10.1.8(eslint@10.0.3):
dependencies:
eslint: 10.0.3
eslint-scope@9.1.2:
dependencies:
'@types/esrecurse': 4.3.1
'@types/estree': 1.0.8
esrecurse: 4.3.0
estraverse: 5.3.0
eslint-visitor-keys@3.4.3: {}
eslint-visitor-keys@5.0.1: {}
eslint@10.0.3:
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3)
'@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.23.3
'@eslint/config-helpers': 0.5.3
'@eslint/core': 1.1.1
'@eslint/plugin-kit': 0.6.1
'@humanfs/node': 0.16.7
'@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.4.3
'@types/estree': 1.0.8
ajv: 6.14.0
cross-spawn: 7.0.6
debug: 4.4.3
escape-string-regexp: 4.0.0
eslint-scope: 9.1.2
eslint-visitor-keys: 5.0.1
espree: 11.2.0
esquery: 1.7.0
esutils: 2.0.3
fast-deep-equal: 3.1.3
file-entry-cache: 8.0.0
find-up: 5.0.0
glob-parent: 6.0.2
ignore: 5.3.2
imurmurhash: 0.1.4
is-glob: 4.0.3
json-stable-stringify-without-jsonify: 1.0.1
minimatch: 10.2.4
natural-compare: 1.4.0
optionator: 0.9.4
transitivePeerDependencies:
- supports-color
espree@11.2.0:
dependencies:
acorn: 8.16.0
acorn-jsx: 5.3.2(acorn@8.16.0)
eslint-visitor-keys: 5.0.1
esquery@1.7.0:
dependencies:
estraverse: 5.3.0
esrecurse@4.3.0:
dependencies:
estraverse: 5.3.0
estraverse@5.3.0: {}
esutils@2.0.3: {}
fast-deep-equal@3.1.3: {}
fast-json-stable-stringify@2.1.0: {}
fast-levenshtein@2.0.6: {}
fdir@6.5.0(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
file-entry-cache@8.0.0:
dependencies:
flat-cache: 4.0.1
find-up@5.0.0:
dependencies:
locate-path: 6.0.0
path-exists: 4.0.0
fix-dts-default-cjs-exports@1.0.1:
dependencies:
magic-string: 0.30.21
mlly: 1.8.0
rollup: 4.59.0
flat-cache@4.0.1:
dependencies:
flatted: 3.3.4
keyv: 4.5.4
flatted@3.3.4: {}
fsevents@2.3.3:
optional: true
glob-parent@6.0.2:
dependencies:
is-glob: 4.0.3
ignore@5.3.2: {}
ignore@7.0.5: {}
imurmurhash@0.1.4: {}
is-extglob@2.1.1: {}
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
isexe@2.0.0: {}
joycon@3.1.1: {}
json-buffer@3.0.1: {}
json-schema-traverse@0.4.1: {}
json-stable-stringify-without-jsonify@1.0.1: {}
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
levn@0.4.1:
dependencies:
prelude-ls: 1.2.1
type-check: 0.4.0
lilconfig@3.1.3: {}
lines-and-columns@1.2.4: {}
load-tsconfig@0.2.5: {}
locate-path@6.0.0:
dependencies:
p-locate: 5.0.0
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
minimatch@10.2.4:
dependencies:
brace-expansion: 5.0.4
mlly@1.8.0:
dependencies:
acorn: 8.16.0
@ -805,8 +1470,31 @@ snapshots:
object-assign: 4.1.1
thenify-all: 1.6.0
natural-compare@1.4.0: {}
object-assign@4.1.1: {}
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
fast-levenshtein: 2.0.6
levn: 0.4.1
prelude-ls: 1.2.1
type-check: 0.4.0
word-wrap: 1.2.5
p-limit@3.1.0:
dependencies:
yocto-queue: 0.1.0
p-locate@5.0.0:
dependencies:
p-limit: 3.1.0
path-exists@4.0.0: {}
path-key@3.1.1: {}
pathe@2.0.3: {}
picocolors@1.1.1: {}
@ -825,6 +1513,12 @@ snapshots:
dependencies:
lilconfig: 3.1.3
prelude-ls@1.2.1: {}
prettier@3.8.1: {}
punycode@2.3.1: {}
readdirp@4.1.2: {}
resolve-from@5.0.0: {}
@ -860,6 +1554,14 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.59.0
fsevents: 2.3.3
semver@7.7.4: {}
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
shebang-regex@3.0.0: {}
source-map@0.7.6: {}
sucrase@3.35.1:
@ -889,6 +1591,10 @@ snapshots:
tree-kill@1.2.2: {}
ts-api-utils@2.4.0(typescript@5.9.3):
dependencies:
typescript: 5.9.3
ts-interface-checker@0.1.13: {}
tsup@8.5.1(typescript@5.9.3):
@ -918,8 +1624,35 @@ snapshots:
- tsx
- yaml
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
typescript-eslint@8.56.1(eslint@10.0.3)(typescript@5.9.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.3)(typescript@5.9.3))(eslint@10.0.3)(typescript@5.9.3)
'@typescript-eslint/parser': 8.56.1(eslint@10.0.3)(typescript@5.9.3)
'@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
'@typescript-eslint/utils': 8.56.1(eslint@10.0.3)(typescript@5.9.3)
eslint: 10.0.3
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
typescript@5.9.3: {}
ufo@1.6.3: {}
undici-types@6.21.0: {}
uri-js@4.4.1:
dependencies:
punycode: 2.3.1
which@2.0.2:
dependencies:
isexe: 2.0.0
word-wrap@1.2.5: {}
yocto-queue@0.1.0: {}

View file

@ -14,6 +14,10 @@ const SYNODIC_SECONDS = SYNODIC_MONTH * 24 * 60 * 60;
* @returns A zero-padded filename string, e.g. `"354.webp"`.
*/
export function cycleMonth(date: Date = new Date()): string {
if (!(date instanceof Date) || isNaN(date.getTime())) {
throw new TypeError('date must be a valid Date instance');
}
// Seconds elapsed since the known new moon anchor
const elapsed = (date.getTime() - MONTH_ANCHOR.getTime()) / 1000;

View file

@ -15,6 +15,10 @@ import { YEAR_IMAGES, YEAR_ANCHOR } from './types.js';
* @returns A zero-padded filename string, e.g. `"4380.webp"`.
*/
export function cycleYear(date: Date = new Date()): string {
if (!(date instanceof Date) || isNaN(date.getTime())) {
throw new TypeError('date must be a valid Date instance');
}
// Hours elapsed since 2023-01-01T00:00:00Z
const elapsed_hours = (date.getTime() - YEAR_ANCHOR.getTime()) / (1000 * 3600);

View file

@ -38,7 +38,7 @@ export function cdnUrl(
set: ImageSet,
size: ImageSize,
quality: ImageQuality,
ref: string = 'main'
ref: string = 'main',
): string {
const folder = imageFolder(set, size, quality);
return `https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@${ref}/${folder}/${filename}`;

View file

@ -5,6 +5,7 @@
'use strict';
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const {
cycleMonth,
@ -18,62 +19,57 @@ const {
YEAR_ANCHOR,
} = require('./dist/index.cjs');
let passed = 0;
let total = 0;
describe('CJS exports', () => {
it('all exports are available via require()', () => {
assert.strictEqual(typeof cycleMonth, 'function');
assert.strictEqual(typeof cycleYear, 'function');
assert.strictEqual(typeof imageFolder, 'function');
assert.strictEqual(typeof cdnUrl, 'function');
assert.strictEqual(typeof SYNODIC_MONTH, 'number');
assert.strictEqual(typeof MONTH_IMAGES, 'number');
assert.strictEqual(typeof YEAR_IMAGES, 'number');
assert(MONTH_ANCHOR instanceof Date);
assert(YEAR_ANCHOR instanceof Date);
});
function test(name, fn) {
total++;
try {
fn();
console.log(`[${name}]... PASS`);
passed++;
} catch (err) {
console.error(`[${name}]... FAIL: ${err.message}`);
process.exitCode = 1;
}
}
it('cycleMonth at anchor returns 001.webp', () => {
assert.strictEqual(cycleMonth(MONTH_ANCHOR), '001.webp');
});
test('exports are available via require()', () => {
assert.strictEqual(typeof cycleMonth, 'function');
assert.strictEqual(typeof cycleYear, 'function');
assert.strictEqual(typeof imageFolder, 'function');
assert.strictEqual(typeof cdnUrl, 'function');
assert.strictEqual(typeof SYNODIC_MONTH, 'number');
assert.strictEqual(typeof MONTH_IMAGES, 'number');
assert.strictEqual(typeof YEAR_IMAGES, 'number');
assert(MONTH_ANCHOR instanceof Date);
assert(YEAR_ANCHOR instanceof Date);
it('cycleYear at anchor returns 0001.webp', () => {
assert.strictEqual(cycleYear(YEAR_ANCHOR), '0001.webp');
});
it('cycleMonth result format is correct', () => {
assert.match(cycleMonth(), /^\d{3}\.webp$/);
});
it('cycleYear result format is correct', () => {
assert.match(cycleYear(), /^\d{4}\.webp$/);
});
it('imageFolder constructs correct path', () => {
assert.strictEqual(imageFolder('mm', 256, 75), 'mm-256-75');
});
it('cdnUrl returns expected jsDelivr URL', () => {
assert.strictEqual(
cdnUrl('001.webp', 'mm', 256, 75),
'https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@main/mm-256-75/001.webp',
);
});
it('cycleMonth throws TypeError for invalid Date', () => {
assert.throws(() => cycleMonth(new Date('invalid')), {
name: 'TypeError',
message: 'date must be a valid Date instance',
});
});
it('cycleYear throws TypeError for invalid Date', () => {
assert.throws(() => cycleYear(new Date('invalid')), {
name: 'TypeError',
message: 'date must be a valid Date instance',
});
});
});
test('cycleMonth at anchor returns 001.webp', () => {
assert.strictEqual(cycleMonth(MONTH_ANCHOR), '001.webp');
});
test('cycleYear at anchor returns 0001.webp', () => {
assert.strictEqual(cycleYear(YEAR_ANCHOR), '0001.webp');
});
test('cycleMonth result format is correct', () => {
const result = cycleMonth();
assert.match(result, /^\d{3}\.webp$/);
});
test('cycleYear result format is correct', () => {
const result = cycleYear();
assert.match(result, /^\d{4}\.webp$/);
});
test('imageFolder constructs correct path', () => {
assert.strictEqual(imageFolder('mm', 256, 75), 'mm-256-75');
});
test('cdnUrl returns expected jsDelivr URL', () => {
const url = cdnUrl('001.webp', 'mm', 256, 75);
assert.strictEqual(
url,
'https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@main/mm-256-75/001.webp'
);
});
console.log(`\n${passed}/${total} tests passed`);
if (passed < total) process.exit(1);

373
test.mjs
View file

@ -1,8 +1,9 @@
/**
* ESM test suite for moon-cycle v2.
* Uses Node.js built-in assert no test framework required.
* Uses Node.js built-in test runner.
*/
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
cycleMonth,
@ -16,225 +17,211 @@ import {
YEAR_ANCHOR,
} from './dist/index.mjs';
let passed = 0;
let total = 0;
function test(name, fn) {
total++;
try {
fn();
console.log(`[${name}]... PASS`);
passed++;
} catch (err) {
console.error(`[${name}]... FAIL: ${err.message}`);
process.exitCode = 1;
}
}
// ─── Constants ───────────────────────────────────────────────────────────────
test('SYNODIC_MONTH is correct IAU value', () => {
assert.strictEqual(SYNODIC_MONTH, 29.53058821398858);
describe('constants', () => {
it('SYNODIC_MONTH is correct IAU value', () => {
assert.strictEqual(SYNODIC_MONTH, 29.53058821398858);
});
it('MONTH_IMAGES is 708', () => {
assert.strictEqual(MONTH_IMAGES, 708);
});
it('YEAR_IMAGES is 8760', () => {
assert.strictEqual(YEAR_IMAGES, 8760);
});
it('MONTH_ANCHOR is 2023-11-13T09:27:00Z', () => {
assert.strictEqual(MONTH_ANCHOR.toISOString(), '2023-11-13T09:27:00.000Z');
});
it('YEAR_ANCHOR is 2023-01-01T00:00:00Z', () => {
assert.strictEqual(YEAR_ANCHOR.toISOString(), '2023-01-01T00:00:00.000Z');
});
});
test('MONTH_IMAGES is 708', () => {
assert.strictEqual(MONTH_IMAGES, 708);
});
// ─── cycleMonth ──────────────────────────────────────────────────────────────
test('YEAR_IMAGES is 8760', () => {
assert.strictEqual(YEAR_IMAGES, 8760);
});
describe('cycleMonth', () => {
it('returns a string', () => {
assert.strictEqual(typeof cycleMonth(), 'string');
});
test('MONTH_ANCHOR is 2023-11-13T09:27:00Z', () => {
assert.strictEqual(MONTH_ANCHOR.toISOString(), '2023-11-13T09:27:00.000Z');
});
it('result matches /^\\d{3}\\.webp$/', () => {
assert.match(cycleMonth(), /^\d{3}\.webp$/);
});
test('YEAR_ANCHOR is 2023-01-01T00:00:00Z', () => {
assert.strictEqual(YEAR_ANCHOR.toISOString(), '2023-01-01T00:00:00.000Z');
});
it('result is never 000.webp (images are 1-indexed)', () => {
const anchor = MONTH_ANCHOR.getTime();
const synodic_ms = SYNODIC_MONTH * 24 * 60 * 60 * 1000;
for (let i = 0; i < 100; i++) {
const date = new Date(anchor + i * (synodic_ms / 100));
assert.notStrictEqual(cycleMonth(date), '000.webp', `Got 000.webp for offset ${i}`);
}
});
// ─── cycleMonth: return format ────────────────────────────────────────────────
it('at anchor date returns 001.webp (start of synodic cycle)', () => {
assert.strictEqual(cycleMonth(MONTH_ANCHOR), '001.webp');
});
test('cycleMonth returns a string', () => {
assert.strictEqual(typeof cycleMonth(), 'string');
});
it('one synodic month + 1 min after anchor returns near start of next cycle', () => {
const oneMonthPlus = new Date(
MONTH_ANCHOR.getTime() + SYNODIC_MONTH * 24 * 60 * 60 * 1000 + 60_000,
);
const index = parseInt(cycleMonth(oneMonthPlus).replace('.webp', ''), 10);
assert(index <= 3, `Expected near start of next cycle (index <= 3), got ${index}`);
});
test('cycleMonth result matches /^\\d{3}\\.webp$/', () => {
const result = cycleMonth();
assert.match(result, /^\d{3}\.webp$/);
});
it('at halfway through synodic month returns ~354.webp', () => {
const half = new Date(MONTH_ANCHOR.getTime() + (SYNODIC_MONTH / 2) * 24 * 60 * 60 * 1000);
const index = parseInt(cycleMonth(half).replace('.webp', ''), 10);
assert(index >= 353 && index <= 355, `Expected ~354, got ${index}`);
});
test('cycleMonth result is never 000.webp (images are 1-indexed)', () => {
// Test many dates across different lunar phases
const anchor = MONTH_ANCHOR.getTime();
const synodic_ms = SYNODIC_MONTH * 24 * 60 * 60 * 1000;
for (let i = 0; i < 100; i++) {
const date = new Date(anchor + i * (synodic_ms / 100));
const result = cycleMonth(date);
assert.notStrictEqual(result, '000.webp', `Got 000.webp for offset ${i}`);
}
});
it('result is always in range [001, 708]', () => {
const start = new Date('2020-01-01T00:00:00Z');
const step = 24 * 60 * 60 * 1000 * 7;
for (let i = 0; i < 260; i++) {
const date = new Date(start.getTime() + i * step);
const index = parseInt(cycleMonth(date).replace('.webp', ''), 10);
assert(index >= 1, `Index ${index} below minimum (date: ${date.toISOString()})`);
assert(index <= 708, `Index ${index} above maximum (date: ${date.toISOString()})`);
}
});
// ─── cycleMonth: anchor date ──────────────────────────────────────────────────
test('cycleMonth at anchor date returns 001.webp (start of synodic cycle)', () => {
assert.strictEqual(cycleMonth(MONTH_ANCHOR), '001.webp');
});
test('cycleMonth one synodic month + 1 min after anchor returns near start of next cycle', () => {
// Adding exactly one synodic month in floating-point can land on either side of the
// cycle boundary (001 or 708) due to IEEE 754 rounding. Adding 1 minute steps past it.
const oneMonthPlus = new Date(
MONTH_ANCHOR.getTime() + SYNODIC_MONTH * 24 * 60 * 60 * 1000 + 60_000
);
const result = cycleMonth(oneMonthPlus);
const index = parseInt(result.replace('.webp', ''), 10);
assert(index <= 3, `Expected near start of next cycle (index <= 3), got ${index}`);
});
test('cycleMonth at halfway through synodic month returns ~354.webp', () => {
const half = new Date(
MONTH_ANCHOR.getTime() + (SYNODIC_MONTH / 2) * 24 * 60 * 60 * 1000
);
const result = cycleMonth(half);
const index = parseInt(result.replace('.webp', ''), 10);
// Allow ±1 for rounding
assert(index >= 353 && index <= 355, `Expected ~354, got ${index}`);
});
test('cycleMonth result is always in range [001, 708]', () => {
// Test dates spanning 5 years
const start = new Date('2020-01-01T00:00:00Z');
const step = 24 * 60 * 60 * 1000 * 7; // weekly
for (let i = 0; i < 260; i++) {
const date = new Date(start.getTime() + i * step);
const result = cycleMonth(date);
it('handles dates before the anchor (pre-2023)', () => {
const past = new Date('2020-06-15T00:00:00Z');
const result = cycleMonth(past);
assert.match(result, /^\d{3}\.webp$/);
const index = parseInt(result.replace('.webp', ''), 10);
assert(index >= 1, `Index ${index} below minimum (date: ${date.toISOString()})`);
assert(index <= 708, `Index ${index} above maximum (date: ${date.toISOString()})`);
}
assert(index >= 1 && index <= 708);
});
it('with no args returns a valid result', () => {
assert.match(cycleMonth(), /^\d{3}\.webp$/);
});
it('throws TypeError for invalid Date', () => {
assert.throws(() => cycleMonth(new Date('invalid')), {
name: 'TypeError',
message: 'date must be a valid Date instance',
});
});
it('throws TypeError for non-Date input', () => {
assert.throws(() => cycleMonth('2023-01-01'), {
name: 'TypeError',
message: 'date must be a valid Date instance',
});
});
});
// ─── cycleMonth: past dates ───────────────────────────────────────────────────
// ─── cycleYear ───────────────────────────────────────────────────────────────
test('cycleMonth handles dates before the anchor (pre-2023)', () => {
const past = new Date('2020-06-15T00:00:00Z');
const result = cycleMonth(past);
assert.match(result, /^\d{3}\.webp$/);
const index = parseInt(result.replace('.webp', ''), 10);
assert(index >= 1 && index <= 708);
});
describe('cycleYear', () => {
it('returns a string', () => {
assert.strictEqual(typeof cycleYear(), 'string');
});
// ─── cycleYear: return format ─────────────────────────────────────────────────
it('result matches /^\\d{4}\\.webp$/', () => {
assert.match(cycleYear(), /^\d{4}\.webp$/);
});
test('cycleYear returns a string', () => {
assert.strictEqual(typeof cycleYear(), 'string');
});
it('result is never 0000.webp (images are 1-indexed)', () => {
const anchor = YEAR_ANCHOR.getTime();
const year_ms = YEAR_IMAGES * 60 * 60 * 1000;
for (let i = 0; i < 100; i++) {
const date = new Date(anchor + i * (year_ms / 100));
assert.notStrictEqual(cycleYear(date), '0000.webp', `Got 0000.webp for offset ${i}`);
}
});
test('cycleYear result matches /^\\d{4}\\.webp$/', () => {
const result = cycleYear();
assert.match(result, /^\d{4}\.webp$/);
});
it('at anchor date returns 0001.webp (start of year)', () => {
assert.strictEqual(cycleYear(YEAR_ANCHOR), '0001.webp');
});
test('cycleYear result is never 0000.webp (images are 1-indexed)', () => {
const anchor = YEAR_ANCHOR.getTime();
const year_ms = YEAR_IMAGES * 60 * 60 * 1000;
for (let i = 0; i < 100; i++) {
const date = new Date(anchor + i * (year_ms / 100));
const result = cycleYear(date);
assert.notStrictEqual(result, '0000.webp', `Got 0000.webp for offset ${i}`);
}
});
it('at exactly one year after anchor returns 0001.webp', () => {
const oneYear = new Date(YEAR_ANCHOR.getTime() + YEAR_IMAGES * 60 * 60 * 1000);
assert.strictEqual(cycleYear(oneYear), '0001.webp');
});
// ─── cycleYear: anchor date ───────────────────────────────────────────────────
it('at halfway through year returns ~4380.webp', () => {
const half = new Date(YEAR_ANCHOR.getTime() + (YEAR_IMAGES / 2) * 60 * 60 * 1000);
const index = parseInt(cycleYear(half).replace('.webp', ''), 10);
assert(index >= 4379 && index <= 4381, `Expected ~4380, got ${index}`);
});
test('cycleYear at anchor date returns 0001.webp (start of year)', () => {
assert.strictEqual(cycleYear(YEAR_ANCHOR), '0001.webp');
});
it('result is always in range [0001, 8760]', () => {
const start = new Date('2020-01-01T00:00:00Z');
const step = 24 * 60 * 60 * 1000 * 7;
for (let i = 0; i < 260; i++) {
const date = new Date(start.getTime() + i * step);
const index = parseInt(cycleYear(date).replace('.webp', ''), 10);
assert(index >= 1, `Index ${index} below minimum`);
assert(index <= 8760, `Index ${index} above maximum`);
}
});
test('cycleYear at exactly one year after anchor returns 0001.webp', () => {
const oneYear = new Date(
YEAR_ANCHOR.getTime() + YEAR_IMAGES * 60 * 60 * 1000
);
assert.strictEqual(cycleYear(oneYear), '0001.webp');
});
test('cycleYear at halfway through year returns ~4380.webp', () => {
const half = new Date(
YEAR_ANCHOR.getTime() + (YEAR_IMAGES / 2) * 60 * 60 * 1000
);
const result = cycleYear(half);
const index = parseInt(result.replace('.webp', ''), 10);
assert(index >= 4379 && index <= 4381, `Expected ~4380, got ${index}`);
});
test('cycleYear result is always in range [0001, 8760]', () => {
const start = new Date('2020-01-01T00:00:00Z');
const step = 24 * 60 * 60 * 1000 * 7;
for (let i = 0; i < 260; i++) {
const date = new Date(start.getTime() + i * step);
const result = cycleYear(date);
it('handles dates before 2023', () => {
const past = new Date('2021-06-15T00:00:00Z');
const result = cycleYear(past);
assert.match(result, /^\d{4}\.webp$/);
const index = parseInt(result.replace('.webp', ''), 10);
assert(index >= 1, `Index ${index} below minimum`);
assert(index <= 8760, `Index ${index} above maximum`);
}
assert(index >= 1 && index <= 8760);
});
it('with no args returns a valid result', () => {
assert.match(cycleYear(), /^\d{4}\.webp$/);
});
it('throws TypeError for invalid Date', () => {
assert.throws(() => cycleYear(new Date('invalid')), {
name: 'TypeError',
message: 'date must be a valid Date instance',
});
});
it('throws TypeError for non-Date input', () => {
assert.throws(() => cycleYear('2023-01-01'), {
name: 'TypeError',
message: 'date must be a valid Date instance',
});
});
});
test('cycleYear handles dates before 2023', () => {
const past = new Date('2021-06-15T00:00:00Z');
const result = cycleYear(past);
assert.match(result, /^\d{4}\.webp$/);
const index = parseInt(result.replace('.webp', ''), 10);
assert(index >= 1 && index <= 8760);
// ─── imageFolder ─────────────────────────────────────────────────────────────
describe('imageFolder', () => {
it('returns correct folder name', () => {
assert.strictEqual(imageFolder('mm', 256, 75), 'mm-256-75');
assert.strictEqual(imageFolder('mm', 512, 85), 'mm-512-85');
assert.strictEqual(imageFolder('my', 256, 75), 'my-256-75');
assert.strictEqual(imageFolder('my', 512, 85), 'my-512-85');
});
});
// ─── cycleMonth/cycleYear default parameter ───────────────────────────────────
// ─── cdnUrl ──────────────────────────────────────────────────────────────────
test('cycleMonth() with no args returns a valid result', () => {
const result = cycleMonth();
assert.match(result, /^\d{3}\.webp$/);
describe('cdnUrl', () => {
it('returns a valid jsDelivr URL', () => {
assert.strictEqual(
cdnUrl('354.webp', 'mm', 256, 75),
'https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@main/mm-256-75/354.webp',
);
});
it('respects custom ref parameter', () => {
assert.strictEqual(
cdnUrl('001.webp', 'my', 512, 85, 'v2.0.0'),
'https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@v2.0.0/my-512-85/001.webp',
);
});
it('integrates with cycleMonth output', () => {
const filename = cycleMonth(MONTH_ANCHOR);
const url = cdnUrl(filename, 'mm', 256, 75);
assert(url.startsWith('https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@main/mm-256-75/'));
assert(url.endsWith('.webp'));
});
});
test('cycleYear() with no args returns a valid result', () => {
const result = cycleYear();
assert.match(result, /^\d{4}\.webp$/);
});
// ─── imageFolder ──────────────────────────────────────────────────────────────
test('imageFolder returns correct folder name', () => {
assert.strictEqual(imageFolder('mm', 256, 75), 'mm-256-75');
assert.strictEqual(imageFolder('mm', 512, 85), 'mm-512-85');
assert.strictEqual(imageFolder('my', 256, 75), 'my-256-75');
assert.strictEqual(imageFolder('my', 512, 85), 'my-512-85');
});
// ─── cdnUrl ───────────────────────────────────────────────────────────────────
test('cdnUrl returns a valid jsDelivr URL', () => {
const url = cdnUrl('354.webp', 'mm', 256, 75);
assert.strictEqual(
url,
'https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@main/mm-256-75/354.webp'
);
});
test('cdnUrl respects custom ref parameter', () => {
const url = cdnUrl('001.webp', 'my', 512, 85, 'v2.0.0');
assert.strictEqual(
url,
'https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@v2.0.0/my-512-85/001.webp'
);
});
test('cdnUrl integrates with cycleMonth output', () => {
const filename = cycleMonth(MONTH_ANCHOR);
const url = cdnUrl(filename, 'mm', 256, 75);
assert(url.startsWith('https://cdn.jsdelivr.net/gh/acamarata/moon-cycle@main/mm-256-75/'));
assert(url.endsWith('.webp'));
});
// ─── Summary ─────────────────────────────────────────────────────────────────
console.log(`\n${passed}/${total} tests passed`);
if (passed < total) process.exit(1);

View file

@ -11,7 +11,10 @@
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"types": ["node"]
"types": ["node"],
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]