Fast and tiny JavaScript/TypeScript cron parser with timezone support
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

init implementation

+4244
+3
.oxfmtrc.json
··· 1 + { 2 + "ignorePatterns": [] 3 + }
+40
.oxlintrc.json
··· 1 + { 2 + "$schema": "./node_modules/oxlint/configuration_schema.json", 3 + "plugins": null, 4 + "categories": {}, 5 + "rules": {}, 6 + "settings": { 7 + "jsx-a11y": { 8 + "polymorphicPropName": null, 9 + "components": {}, 10 + "attributes": {} 11 + }, 12 + "next": { 13 + "rootDir": [] 14 + }, 15 + "react": { 16 + "formComponents": [], 17 + "linkComponents": [], 18 + "version": null, 19 + "componentWrapperFunctions": [] 20 + }, 21 + "jsdoc": { 22 + "ignorePrivate": false, 23 + "ignoreInternal": false, 24 + "ignoreReplacesDocs": true, 25 + "overrideReplacesDocs": true, 26 + "augmentsExtendsReplacesDocs": false, 27 + "implementsReplacesDocs": false, 28 + "exemptDestructuredRootsFromChecks": false, 29 + "tagNamePreference": {} 30 + }, 31 + "vitest": { 32 + "typecheck": false 33 + } 34 + }, 35 + "env": { 36 + "builtin": true 37 + }, 38 + "globals": {}, 39 + "ignorePatterns": [] 40 + }
+21
LICENSE
··· 1 + MIT License 2 + 3 + Copyright (c) 2026 Kasparas Bilkis 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy 6 + of this software and associated documentation files (the "Software"), to deal 7 + in the Software without restriction, including without limitation the rights 8 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + copies of the Software, and to permit persons to whom the Software is 10 + furnished to do so, subject to the following conditions: 11 + 12 + The above copyright notice and this permission notice shall be included in all 13 + copies or substantial portions of the Software. 14 + 15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 + SOFTWARE.
+236
README.md
··· 1 + # cron-fast 2 + 3 + A fast and lightweight cron parser with timezone support and zero dependencies. 4 + 5 + ## Features 6 + 7 + - **Lightweight** - Zero dependencies 8 + - **Fast** - Optimal field increment algorithm 9 + - **Tree-shakeable** - Import only what you need 10 + - **Timezone support** - Built-in timezone handling using native `Intl` 11 + - **Modern** - ESM + CJS, TypeScript-first 12 + - **Fully tested** - Comprehensive test coverage 13 + - **Simple API** - Clean, intuitive interface 14 + - **ISO 8601 compatible** - Works with all standard date formats 15 + 16 + ## Installation 17 + 18 + ```bash 19 + # npm 20 + npm install cron-fast 21 + 22 + # pnpm 23 + pnpm add cron-fast 24 + 25 + # yarn 26 + yarn add cron-fast 27 + 28 + # jsr 29 + npx jsr add @kbilkis/cron-fast 30 + 31 + # deno 32 + deno add jsr:@kbilkis/cron-fast 33 + ``` 34 + 35 + ## Quick Start 36 + 37 + ```typescript 38 + import { nextRun, previousRun, isValid } from "cron-fast"; 39 + 40 + // Get next execution time (UTC) 41 + const next = nextRun("0 9 * * *"); 42 + console.log(next); // Next 9:00 AM UTC 43 + 44 + // With timezone 45 + const nextNY = nextRun("0 9 * * *", { timezone: "America/New_York" }); 46 + console.log(nextNY); // Next 9:00 AM Eastern Time 47 + 48 + // Get previous execution 49 + const prev = previousRun("*/15 * * * *"); 50 + 51 + // Validate expression 52 + if (isValid("0 9 * * *")) { 53 + console.log("Valid cron expression!"); 54 + } 55 + ``` 56 + 57 + ## API 58 + 59 + ### `nextRun(expression, options?)` 60 + 61 + Get the next execution time for a cron expression. 62 + 63 + ```typescript 64 + nextRun("0 9 * * *"); // Next 9:00 AM UTC 65 + nextRun("0 9 * * *", { timezone: "Europe/London" }); // Next 9:00 AM London time 66 + nextRun("0 9 * * *", { from: new Date("2026-03-15") }); // Next after Mar 15, 2026 67 + ``` 68 + 69 + ### `previousRun(expression, options?)` 70 + 71 + Get the previous execution time. 72 + 73 + ```typescript 74 + previousRun("0 9 * * *"); // Last 9:00 AM UTC 75 + previousRun("0 9 * * *", { timezone: "Asia/Tokyo" }); 76 + ``` 77 + 78 + ### `nextRuns(expression, count, options?)` 79 + 80 + Get next N execution times. 81 + 82 + ```typescript 83 + nextRuns("0 9 * * *", 5); // Next 5 occurrences 84 + ``` 85 + 86 + ### `isValid(expression)` 87 + 88 + Validate a cron expression. 89 + 90 + ```typescript 91 + isValid("0 9 * * *"); // true 92 + isValid("invalid"); // false 93 + ``` 94 + 95 + ### `isMatch(expression, date, options?)` 96 + 97 + Check if a date matches the cron expression. 98 + 99 + ```typescript 100 + isMatch("0 9 * * *", new Date("2026-03-15T09:00:00Z")); // true 101 + ``` 102 + 103 + ### `parse(expression)` 104 + 105 + Parse a cron expression into its components. 106 + 107 + ```typescript 108 + parse("0 9 * * 1-5"); 109 + // Returns: { minute: [0], hour: [9], day: [1, 2, ..., 31], month: [0, 1, 2, ..., 11], weekday: [1,2,3,4,5] } 110 + ``` 111 + 112 + ## Cron Expression Format 113 + 114 + ``` 115 + * * * * * 116 + │ │ │ │ │ 117 + │ │ │ │ └─ Day of Week (0-7, SUN-SAT) 118 + │ │ │ └─── Month (1-12, JAN-DEC) 119 + │ │ └───── Day of Month (1-31) 120 + │ └─────── Hour (0-23) 121 + └───────── Minute (0-59) 122 + ``` 123 + 124 + ### Supported Special Characters 125 + 126 + - `*` - Any value 127 + - `,` - Value list (e.g., `1,3,5`) 128 + - `-` - Range (e.g., `1-5`) 129 + - `/` - Step values (e.g., `*/5`) 130 + 131 + ## ISO 8601 Date Support 132 + 133 + cron-fast fully supports ISO 8601 date formats for input: 134 + 135 + ```typescript 136 + // All these formats work: 137 + nextRun("0 9 * * *", { from: new Date("2026-03-15T14:30:00Z") }); // UTC 138 + nextRun("0 9 * * *", { from: new Date("2026-03-15T09:30:00-05:00") }); // With offset 139 + nextRun("0 9 * * *", { from: new Date("2026-03-15T14:30:00.500Z") }); // With milliseconds 140 + 141 + // Different representations of the same moment produce identical results 142 + const utc = new Date("2026-03-15T14:30:00Z"); 143 + const est = new Date("2026-03-15T09:30:00-05:00"); // Same moment 144 + nextRun("0 9 * * *", { from: utc }).getTime() === nextRun("0 9 * * *", { from: est }).getTime(); // true 145 + ``` 146 + 147 + **Note:** All returned Date objects are in UTC (ending with `Z` in `.toISOString()`). Use `.toLocaleString()` to display in any timezone. 148 + 149 + ## Tree-Shaking 150 + 151 + cron-fast is fully tree-shakeable. Import only what you need: 152 + 153 + ```typescript 154 + // Small bundle - only validation 155 + import { isValid } from "cron-fast"; 156 + 157 + // Medium bundle - one function + dependencies 158 + import { nextRun } from "cron-fast"; 159 + 160 + // Full bundle - everything 161 + import * as cron from "cron-fast"; 162 + ``` 163 + 164 + ## Advanced Usage 165 + 166 + ### Working with Timezones 167 + 168 + ```typescript 169 + // Cron expression is interpreted in the specified timezone 170 + const next = nextRun("0 9 * * *", { timezone: "America/New_York" }); 171 + 172 + // The returned Date is always UTC internally 173 + console.log(next.toISOString()); // "2026-03-15T13:00:00.000Z" (9 AM EDT = 1 PM UTC) 174 + 175 + // Display in any timezone 176 + console.log(next.toLocaleString("en-US", { timeZone: "America/New_York" })); 177 + // "3/15/2026, 9:00:00 AM" 178 + ``` 179 + 180 + ### Multiple Executions 181 + 182 + ```typescript 183 + // Get next 10 runs 184 + const runs = nextRuns("0 */6 * * *", 10); // Every 6 hours 185 + 186 + // With timezone 187 + const runsNY = nextRuns("0 9 * * 1-5", 5, { timezone: "America/New_York" }); 188 + // Next 5 weekday mornings in New York 189 + ``` 190 + 191 + ### Validation and Parsing 192 + 193 + ```typescript 194 + // Validate before using 195 + if (!isValid(userInput)) { 196 + throw new Error("Invalid cron expression"); 197 + } 198 + 199 + // Parse to see what it means 200 + const parsed = parse("*/15 9-17 * * 1-5"); 201 + console.log(parsed); 202 + // { 203 + // minute: [0, 15, 30, 45], 204 + // hour: [9, 10, 11, 12, 13, 14, 15, 16, 17], 205 + // day: [1-31], 206 + // month: [1-12], 207 + // weekday: [1, 2, 3, 4, 5] 208 + // } 209 + ``` 210 + 211 + ### Check if Date Matches 212 + 213 + ```typescript 214 + const now = new Date(); 215 + 216 + if (isMatch("0 9 * * 1-5", now)) { 217 + console.log("It's 9 AM on a weekday!"); 218 + } 219 + 220 + // With timezone 221 + if (isMatch("0 9 * * *", now, { timezone: "America/New_York" })) { 222 + console.log("It's 9 AM in New York!"); 223 + } 224 + ``` 225 + 226 + ## Tips & Gotchas 227 + 228 + - **Timezone handling**: The cron expression is interpreted in the timezone you specify, but the returned Date is always in UTC 229 + - **Daylight saving time**: Use IANA timezone names (like "America/New_York") instead of abbreviations (like "EST") 230 + - **Validation**: Always check `isValid()` before parsing user input 231 + - **Day 0 and 7**: Both represent Sunday in the day-of-week field 232 + - **Ranges are inclusive**: `1-5` includes both 1 and 5 233 + 234 + ## License 235 + 236 + MIT
+58
examples/basic.ts
··· 1 + import { nextRun, previousRun, nextRuns, isValid, parse } from "../src/index.js"; 2 + 3 + console.log("=== cron-fast Examples ===\n"); 4 + 5 + // Example 1: Basic usage 6 + console.log('1. Next run for "0 9 * * *" (9 AM daily):'); 7 + const next = nextRun("0 9 * * *"); 8 + console.log(` ${next.toISOString()}\n`); 9 + 10 + // Example 2: With timezone 11 + console.log('2. Next run for "0 9 * * *" in New York timezone:'); 12 + const nextNY = nextRun("0 9 * * *", { timezone: "America/New_York" }); 13 + console.log(` ${nextNY.toISOString()}`); 14 + console.log(` (${nextNY.toLocaleString("en-US", { timeZone: "America/New_York" })})\n`); 15 + 16 + // Example 3: Every 15 minutes 17 + console.log('3. Next 5 runs for "*/15 * * * *" (every 15 minutes):'); 18 + const runs = nextRuns("*/15 * * * *", 5); 19 + runs.forEach((run, i) => { 20 + console.log(` ${i + 1}. ${run.toISOString()}`); 21 + }); 22 + console.log(); 23 + 24 + // Example 4: Weekdays only 25 + console.log('4. Next run for "0 9 * * 1-5" (9 AM weekdays):'); 26 + const weekdayRun = nextRun("0 9 * * 1-5"); 27 + console.log(` ${weekdayRun.toISOString()}`); 28 + console.log( 29 + ` Day of week: ${["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][weekdayRun.getUTCDay()]}\n`, 30 + ); 31 + 32 + // Example 5: Previous run 33 + console.log('5. Previous run for "0 */6 * * *" (every 6 hours):'); 34 + const prev = previousRun("0 */6 * * *"); 35 + console.log(` ${prev.toISOString()}\n`); 36 + 37 + // Example 6: Validation 38 + console.log("6. Validate cron expressions:"); 39 + console.log(` "0 9 * * *" is valid: ${isValid("0 9 * * *")}`); 40 + console.log(` "invalid" is valid: ${isValid("invalid")}\n`); 41 + 42 + // Example 7: Parse expression 43 + console.log('7. Parse "0 9 * * 1-5":'); 44 + const parsed = parse("0 9 * * 1-5"); 45 + console.log(` Minutes: ${parsed.minute}`); 46 + console.log(` Hours: ${parsed.hour}`); 47 + console.log(` Weekdays: ${parsed.weekday} (Mon-Fri)\n`); 48 + 49 + console.log("=== Performance Test ===\n"); 50 + const iterations = 10000; 51 + const start = performance.now(); 52 + for (let i = 0; i < iterations; i++) { 53 + nextRun("*/15 * * * *"); 54 + } 55 + const end = performance.now(); 56 + const opsPerSec = Math.round((iterations / (end - start)) * 1000); 57 + console.log(`Calculated ${iterations} next runs in ${(end - start).toFixed(2)}ms`); 58 + console.log(`Performance: ${opsPerSec.toLocaleString()} ops/sec`);
+8
jsr.json
··· 1 + { 2 + "name": "@kbilkis/cron-fast", 3 + "version": "0.1.0", 4 + "exports": "./src/index.ts", 5 + "publish": { 6 + "include": ["src/**/*.ts", "README.md", "LICENSE"] 7 + } 8 + }
+66
package.json
··· 1 + { 2 + "name": "cron-fast", 3 + "version": "0.1.0", 4 + "description": "Lightweight, fast cron parser with timezone support - zero dependencies", 5 + "keywords": [ 6 + "cron", 7 + "fast", 8 + "lightweight", 9 + "parser", 10 + "performance", 11 + "schedule", 12 + "timezone", 13 + "zero-dependencies" 14 + ], 15 + "license": "MIT", 16 + "author": "Kasparas Bilkis kasparas@bilkis.lt", 17 + "repository": { 18 + "type": "git", 19 + "url": "https://github.com/kbilkis/cron-fast.git" 20 + }, 21 + "files": [ 22 + "dist", 23 + "README.md", 24 + "LICENSE" 25 + ], 26 + "type": "module", 27 + "sideEffects": false, 28 + "main": "./dist/index.cjs", 29 + "module": "./dist/index.mjs", 30 + "types": "./dist/index.d.mts", 31 + "exports": { 32 + ".": { 33 + "import": { 34 + "types": "./dist/index.d.mts", 35 + "default": "./dist/index.mjs" 36 + }, 37 + "require": { 38 + "types": "./dist/index.d.cts", 39 + "default": "./dist/index.cjs" 40 + } 41 + } 42 + }, 43 + "scripts": { 44 + "build": "tsdown", 45 + "test": "vitest run", 46 + "test:watch": "vitest", 47 + "test:ui": "vitest --ui", 48 + "lint": "oxlint", 49 + "lint:fix": "oxlint --fix", 50 + "fmt": "oxfmt", 51 + "fmt:check": "oxfmt --check", 52 + "typecheck": "tsc --noEmit", 53 + "prepublishOnly": "pnpm run build" 54 + }, 55 + "devDependencies": { 56 + "@types/node": "^25.2.0", 57 + "@vitest/ui": "^4.0.18", 58 + "oxfmt": "^0.28.0", 59 + "oxlint": "^1.43.0", 60 + "tsdown": "^0.20.3", 61 + "typescript": "^5.9.3", 62 + "vite": "^7.3.1", 63 + "vitest": "^4.0.18" 64 + }, 65 + "packageManager": "pnpm@10.15.1" 66 + }
+1645
pnpm-lock.yaml
··· 1 + lockfileVersion: '9.0' 2 + 3 + settings: 4 + autoInstallPeers: true 5 + excludeLinksFromLockfile: false 6 + 7 + importers: 8 + 9 + .: 10 + devDependencies: 11 + '@types/node': 12 + specifier: ^25.2.0 13 + version: 25.2.0 14 + '@vitest/ui': 15 + specifier: ^4.0.18 16 + version: 4.0.18(vitest@4.0.18) 17 + oxfmt: 18 + specifier: ^0.28.0 19 + version: 0.28.0 20 + oxlint: 21 + specifier: ^1.43.0 22 + version: 1.43.0 23 + tsdown: 24 + specifier: ^0.20.3 25 + version: 0.20.3(typescript@5.9.3) 26 + typescript: 27 + specifier: ^5.9.3 28 + version: 5.9.3 29 + vite: 30 + specifier: ^7.3.1 31 + version: 7.3.1(@types/node@25.2.0) 32 + vitest: 33 + specifier: ^4.0.18 34 + version: 4.0.18(@types/node@25.2.0)(@vitest/ui@4.0.18) 35 + 36 + packages: 37 + 38 + '@babel/generator@8.0.0-rc.1': 39 + resolution: {integrity: sha512-3ypWOOiC4AYHKr8vYRVtWtWmyvcoItHtVqF8paFax+ydpmUdPsJpLBkBBs5ItmhdrwC3a0ZSqqFAdzls4ODP3w==} 40 + engines: {node: ^20.19.0 || >=22.12.0} 41 + 42 + '@babel/helper-string-parser@8.0.0-rc.1': 43 + resolution: {integrity: sha512-vi/pfmbrOtQmqgfboaBhaCU50G7mcySVu69VU8z+lYoPPB6WzI9VgV7WQfL908M4oeSH5fDkmoupIqoE0SdApw==} 44 + engines: {node: ^20.19.0 || >=22.12.0} 45 + 46 + '@babel/helper-validator-identifier@8.0.0-rc.1': 47 + resolution: {integrity: sha512-I4YnARytXC2RzkLNVnf5qFNFMzp679qZpmtw/V3Jt2uGnWiIxyJtaukjG7R8pSx8nG2NamICpGfljQsogj+FbQ==} 48 + engines: {node: ^20.19.0 || >=22.12.0} 49 + 50 + '@babel/parser@8.0.0-rc.1': 51 + resolution: {integrity: sha512-6HyyU5l1yK/7h9Ki52i5h6mDAx4qJdiLQO4FdCyJNoB/gy3T3GGJdhQzzbZgvgZCugYBvwtQiWRt94QKedHnkA==} 52 + engines: {node: ^20.19.0 || >=22.12.0} 53 + hasBin: true 54 + 55 + '@babel/types@8.0.0-rc.1': 56 + resolution: {integrity: sha512-ubmJ6TShyaD69VE9DQrlXcdkvJbmwWPB8qYj0H2kaJi29O7vJT9ajSdBd2W8CG34pwL9pYA74fi7RHC1qbLoVQ==} 57 + engines: {node: ^20.19.0 || >=22.12.0} 58 + 59 + '@emnapi/core@1.8.1': 60 + resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} 61 + 62 + '@emnapi/runtime@1.8.1': 63 + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} 64 + 65 + '@emnapi/wasi-threads@1.1.0': 66 + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} 67 + 68 + '@esbuild/aix-ppc64@0.27.2': 69 + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} 70 + engines: {node: '>=18'} 71 + cpu: [ppc64] 72 + os: [aix] 73 + 74 + '@esbuild/android-arm64@0.27.2': 75 + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} 76 + engines: {node: '>=18'} 77 + cpu: [arm64] 78 + os: [android] 79 + 80 + '@esbuild/android-arm@0.27.2': 81 + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} 82 + engines: {node: '>=18'} 83 + cpu: [arm] 84 + os: [android] 85 + 86 + '@esbuild/android-x64@0.27.2': 87 + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} 88 + engines: {node: '>=18'} 89 + cpu: [x64] 90 + os: [android] 91 + 92 + '@esbuild/darwin-arm64@0.27.2': 93 + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} 94 + engines: {node: '>=18'} 95 + cpu: [arm64] 96 + os: [darwin] 97 + 98 + '@esbuild/darwin-x64@0.27.2': 99 + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} 100 + engines: {node: '>=18'} 101 + cpu: [x64] 102 + os: [darwin] 103 + 104 + '@esbuild/freebsd-arm64@0.27.2': 105 + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} 106 + engines: {node: '>=18'} 107 + cpu: [arm64] 108 + os: [freebsd] 109 + 110 + '@esbuild/freebsd-x64@0.27.2': 111 + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} 112 + engines: {node: '>=18'} 113 + cpu: [x64] 114 + os: [freebsd] 115 + 116 + '@esbuild/linux-arm64@0.27.2': 117 + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} 118 + engines: {node: '>=18'} 119 + cpu: [arm64] 120 + os: [linux] 121 + 122 + '@esbuild/linux-arm@0.27.2': 123 + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} 124 + engines: {node: '>=18'} 125 + cpu: [arm] 126 + os: [linux] 127 + 128 + '@esbuild/linux-ia32@0.27.2': 129 + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} 130 + engines: {node: '>=18'} 131 + cpu: [ia32] 132 + os: [linux] 133 + 134 + '@esbuild/linux-loong64@0.27.2': 135 + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} 136 + engines: {node: '>=18'} 137 + cpu: [loong64] 138 + os: [linux] 139 + 140 + '@esbuild/linux-mips64el@0.27.2': 141 + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} 142 + engines: {node: '>=18'} 143 + cpu: [mips64el] 144 + os: [linux] 145 + 146 + '@esbuild/linux-ppc64@0.27.2': 147 + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} 148 + engines: {node: '>=18'} 149 + cpu: [ppc64] 150 + os: [linux] 151 + 152 + '@esbuild/linux-riscv64@0.27.2': 153 + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} 154 + engines: {node: '>=18'} 155 + cpu: [riscv64] 156 + os: [linux] 157 + 158 + '@esbuild/linux-s390x@0.27.2': 159 + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} 160 + engines: {node: '>=18'} 161 + cpu: [s390x] 162 + os: [linux] 163 + 164 + '@esbuild/linux-x64@0.27.2': 165 + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} 166 + engines: {node: '>=18'} 167 + cpu: [x64] 168 + os: [linux] 169 + 170 + '@esbuild/netbsd-arm64@0.27.2': 171 + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} 172 + engines: {node: '>=18'} 173 + cpu: [arm64] 174 + os: [netbsd] 175 + 176 + '@esbuild/netbsd-x64@0.27.2': 177 + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} 178 + engines: {node: '>=18'} 179 + cpu: [x64] 180 + os: [netbsd] 181 + 182 + '@esbuild/openbsd-arm64@0.27.2': 183 + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} 184 + engines: {node: '>=18'} 185 + cpu: [arm64] 186 + os: [openbsd] 187 + 188 + '@esbuild/openbsd-x64@0.27.2': 189 + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} 190 + engines: {node: '>=18'} 191 + cpu: [x64] 192 + os: [openbsd] 193 + 194 + '@esbuild/openharmony-arm64@0.27.2': 195 + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} 196 + engines: {node: '>=18'} 197 + cpu: [arm64] 198 + os: [openharmony] 199 + 200 + '@esbuild/sunos-x64@0.27.2': 201 + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} 202 + engines: {node: '>=18'} 203 + cpu: [x64] 204 + os: [sunos] 205 + 206 + '@esbuild/win32-arm64@0.27.2': 207 + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} 208 + engines: {node: '>=18'} 209 + cpu: [arm64] 210 + os: [win32] 211 + 212 + '@esbuild/win32-ia32@0.27.2': 213 + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} 214 + engines: {node: '>=18'} 215 + cpu: [ia32] 216 + os: [win32] 217 + 218 + '@esbuild/win32-x64@0.27.2': 219 + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} 220 + engines: {node: '>=18'} 221 + cpu: [x64] 222 + os: [win32] 223 + 224 + '@jridgewell/gen-mapping@0.3.13': 225 + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} 226 + 227 + '@jridgewell/resolve-uri@3.1.2': 228 + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 229 + engines: {node: '>=6.0.0'} 230 + 231 + '@jridgewell/sourcemap-codec@1.5.5': 232 + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} 233 + 234 + '@jridgewell/trace-mapping@0.3.31': 235 + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} 236 + 237 + '@napi-rs/wasm-runtime@1.1.1': 238 + resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} 239 + 240 + '@oxc-project/types@0.112.0': 241 + resolution: {integrity: sha512-m6RebKHIRsax2iCwVpYW2ErQwa4ywHJrE4sCK3/8JK8ZZAWOKXaRJFl/uP51gaVyyXlaS4+chU1nSCdzYf6QqQ==} 242 + 243 + '@oxfmt/darwin-arm64@0.28.0': 244 + resolution: {integrity: sha512-jmUfF7cNJPw57bEK7sMIqrYRgn4LH428tSgtgLTCtjuGuu1ShREyrkeB7y8HtkXRfhBs4lVY+HMLhqElJvZ6ww==} 245 + cpu: [arm64] 246 + os: [darwin] 247 + 248 + '@oxfmt/darwin-x64@0.28.0': 249 + resolution: {integrity: sha512-S6vlV8S7jbjzJOSjfVg2CimUC0r7/aHDLdUm/3+/B/SU/s1jV7ivqWkMv1/8EB43d1BBwT9JQ60ZMTkBqeXSFA==} 250 + cpu: [x64] 251 + os: [darwin] 252 + 253 + '@oxfmt/linux-arm64-gnu@0.28.0': 254 + resolution: {integrity: sha512-TfJkMZjePbLiskmxFXVAbGI/OZtD+y+fwS0wyW8O6DWG0ARTf0AipY9zGwGoOdpFuXOJceXvN4SHGLbYNDMY4Q==} 255 + cpu: [arm64] 256 + os: [linux] 257 + 258 + '@oxfmt/linux-arm64-musl@0.28.0': 259 + resolution: {integrity: sha512-7fyQUdW203v4WWGr1T3jwTz4L7KX9y5DeATryQ6fLT6QQp9GEuct8/k0lYhd+ys42iTV/IkJF20e3YkfSOOILg==} 260 + cpu: [arm64] 261 + os: [linux] 262 + 263 + '@oxfmt/linux-x64-gnu@0.28.0': 264 + resolution: {integrity: sha512-sRKqAvEonuz0qr1X1ncUZceOBJerKzkO2gZIZmosvy/JmqyffpIFL3OE2tqacFkeDhrC+dNYQpusO8zsfHo3pw==} 265 + cpu: [x64] 266 + os: [linux] 267 + 268 + '@oxfmt/linux-x64-musl@0.28.0': 269 + resolution: {integrity: sha512-fW6czbXutX/tdQe8j4nSIgkUox9RXqjyxwyWXUDItpoDkoXllq17qbD7GVc0whrEhYQC6hFE1UEAcDypLJoSzw==} 270 + cpu: [x64] 271 + os: [linux] 272 + 273 + '@oxfmt/win32-arm64@0.28.0': 274 + resolution: {integrity: sha512-D/HDeQBAQRjTbD9OLV6kRDcStrIfO+JsUODDCdGmhRfNX8LPCx95GpfyybpZfn3wVF8Jq/yjPXV1xLkQ+s7RcA==} 275 + cpu: [arm64] 276 + os: [win32] 277 + 278 + '@oxfmt/win32-x64@0.28.0': 279 + resolution: {integrity: sha512-4+S2j4OxOIyo8dz5osm5dZuL0yVmxXvtmNdHB5xyGwAWVvyWNvf7tCaQD7w2fdSsAXQLOvK7KFQrHFe33nJUCA==} 280 + cpu: [x64] 281 + os: [win32] 282 + 283 + '@oxlint/darwin-arm64@1.43.0': 284 + resolution: {integrity: sha512-C/GhObv/pQZg34NOzB6Mk8x0wc9AKj8fXzJF8ZRKTsBPyHusC6AZ6bba0QG0TUufw1KWuD0j++oebQfWeiFXNw==} 285 + cpu: [arm64] 286 + os: [darwin] 287 + 288 + '@oxlint/darwin-x64@1.43.0': 289 + resolution: {integrity: sha512-4NjfUtEEH8ewRQ2KlZGmm6DyrvypMdHwBnQT92vD0dLScNOQzr0V9O8Ua4IWXdeCNl/XMVhAV3h4/3YEYern5A==} 290 + cpu: [x64] 291 + os: [darwin] 292 + 293 + '@oxlint/linux-arm64-gnu@1.43.0': 294 + resolution: {integrity: sha512-75tf1HvwdZ3ebk83yMbSB+moAEWK98mYqpXiaFAi6Zshie7r+Cx5PLXZFUEqkscenoZ+fcNXakHxfn94V6nf1g==} 295 + cpu: [arm64] 296 + os: [linux] 297 + 298 + '@oxlint/linux-arm64-musl@1.43.0': 299 + resolution: {integrity: sha512-BHV4fb36T2p/7bpA9fiJ5ayt7oJbiYX10nklW5arYp4l9/9yG/FQC5J4G1evzbJ/YbipF9UH0vYBAm5xbqGrvw==} 300 + cpu: [arm64] 301 + os: [linux] 302 + 303 + '@oxlint/linux-x64-gnu@1.43.0': 304 + resolution: {integrity: sha512-1l3nvnzWWse1YHibzZ4HQXdF/ibfbKZhp9IguElni3bBqEyPEyurzZ0ikWynDxKGXqZa+UNXTFuU1NRVX1RJ3g==} 305 + cpu: [x64] 306 + os: [linux] 307 + 308 + '@oxlint/linux-x64-musl@1.43.0': 309 + resolution: {integrity: sha512-+jNYgLGRFTJxJuaSOZJBwlYo5M0TWRw0+3y5MHOL4ArrIdHyCthg6r4RbVWrsR1qUfUE1VSSHQ2bfbC99RXqMg==} 310 + cpu: [x64] 311 + os: [linux] 312 + 313 + '@oxlint/win32-arm64@1.43.0': 314 + resolution: {integrity: sha512-dvs1C/HCjCyGTURMagiHprsOvVTT3omDiSzi5Qw0D4QFJ1pEaNlfBhVnOUYgUfS6O7Mcmj4+G+sidRsQcWQ/kA==} 315 + cpu: [arm64] 316 + os: [win32] 317 + 318 + '@oxlint/win32-x64@1.43.0': 319 + resolution: {integrity: sha512-bSuItSU8mTSDsvmmLTepTdCL2FkJI6dwt9tot/k0EmiYF+ArRzmsl4lXVLssJNRV5lJEc5IViyTrh7oiwrjUqA==} 320 + cpu: [x64] 321 + os: [win32] 322 + 323 + '@polka/url@1.0.0-next.29': 324 + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} 325 + 326 + '@quansync/fs@1.0.0': 327 + resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} 328 + 329 + '@rolldown/binding-android-arm64@1.0.0-rc.3': 330 + resolution: {integrity: sha512-0T1k9FinuBZ/t7rZ8jN6OpUKPnUjNdYHoj/cESWrQ3ZraAJ4OMm6z7QjSfCxqj8mOp9kTKc1zHK3kGz5vMu+nQ==} 331 + engines: {node: ^20.19.0 || >=22.12.0} 332 + cpu: [arm64] 333 + os: [android] 334 + 335 + '@rolldown/binding-darwin-arm64@1.0.0-rc.3': 336 + resolution: {integrity: sha512-JWWLzvcmc/3pe7qdJqPpuPk91SoE/N+f3PcWx/6ZwuyDVyungAEJPvKm/eEldiDdwTmaEzWfIR+HORxYWrCi1A==} 337 + engines: {node: ^20.19.0 || >=22.12.0} 338 + cpu: [arm64] 339 + os: [darwin] 340 + 341 + '@rolldown/binding-darwin-x64@1.0.0-rc.3': 342 + resolution: {integrity: sha512-MTakBxfx3tde5WSmbHxuqlDsIW0EzQym+PJYGF4P6lG2NmKzi128OGynoFUqoD5ryCySEY85dug4v+LWGBElIw==} 343 + engines: {node: ^20.19.0 || >=22.12.0} 344 + cpu: [x64] 345 + os: [darwin] 346 + 347 + '@rolldown/binding-freebsd-x64@1.0.0-rc.3': 348 + resolution: {integrity: sha512-jje3oopyOLs7IwfvXoS6Lxnmie5JJO7vW29fdGFu5YGY1EDbVDhD+P9vDihqS5X6fFiqL3ZQZCMBg6jyHkSVww==} 349 + engines: {node: ^20.19.0 || >=22.12.0} 350 + cpu: [x64] 351 + os: [freebsd] 352 + 353 + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.3': 354 + resolution: {integrity: sha512-A0n8P3hdLAaqzSFrQoA42p23ZKBYQOw+8EH5r15Sa9X1kD9/JXe0YT2gph2QTWvdr0CVK2BOXiK6ENfy6DXOag==} 355 + engines: {node: ^20.19.0 || >=22.12.0} 356 + cpu: [arm] 357 + os: [linux] 358 + 359 + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.3': 360 + resolution: {integrity: sha512-kWXkoxxarYISBJ4bLNf5vFkEbb4JvccOwxWDxuK9yee8lg5XA7OpvlTptfRuwEvYcOZf+7VS69Uenpmpyo5Bjw==} 361 + engines: {node: ^20.19.0 || >=22.12.0} 362 + cpu: [arm64] 363 + os: [linux] 364 + 365 + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.3': 366 + resolution: {integrity: sha512-Z03/wrqau9Bicfgb3Dbs6SYTHliELk2PM2LpG2nFd+cGupTMF5kanLEcj2vuuJLLhptNyS61rtk7SOZ+lPsTUA==} 367 + engines: {node: ^20.19.0 || >=22.12.0} 368 + cpu: [arm64] 369 + os: [linux] 370 + 371 + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.3': 372 + resolution: {integrity: sha512-iSXXZsQp08CSilff/DCTFZHSVEpEwdicV3W8idHyrByrcsRDVh9sGC3sev6d8BygSGj3vt8GvUKBPCoyMA4tgQ==} 373 + engines: {node: ^20.19.0 || >=22.12.0} 374 + cpu: [x64] 375 + os: [linux] 376 + 377 + '@rolldown/binding-linux-x64-musl@1.0.0-rc.3': 378 + resolution: {integrity: sha512-qaj+MFudtdCv9xZo9znFvkgoajLdc+vwf0Kz5N44g+LU5XMe+IsACgn3UG7uTRlCCvhMAGXm1XlpEA5bZBrOcw==} 379 + engines: {node: ^20.19.0 || >=22.12.0} 380 + cpu: [x64] 381 + os: [linux] 382 + 383 + '@rolldown/binding-openharmony-arm64@1.0.0-rc.3': 384 + resolution: {integrity: sha512-U662UnMETyjT65gFmG9ma+XziENrs7BBnENi/27swZPYagubfHRirXHG2oMl+pEax2WvO7Kb9gHZmMakpYqBHQ==} 385 + engines: {node: ^20.19.0 || >=22.12.0} 386 + cpu: [arm64] 387 + os: [openharmony] 388 + 389 + '@rolldown/binding-wasm32-wasi@1.0.0-rc.3': 390 + resolution: {integrity: sha512-gekrQ3Q2HiC1T5njGyuUJoGpK/l6B/TNXKed3fZXNf9YRTJn3L5MOZsFBn4bN2+UX+8+7hgdlTcEsexX988G4g==} 391 + engines: {node: '>=14.0.0'} 392 + cpu: [wasm32] 393 + 394 + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.3': 395 + resolution: {integrity: sha512-85y5JifyMgs8m5K2XzR/VDsapKbiFiohl7s5lEj7nmNGO0pkTXE7q6TQScei96BNAsoK7JC3pA7ukA8WRHVJpg==} 396 + engines: {node: ^20.19.0 || >=22.12.0} 397 + cpu: [arm64] 398 + os: [win32] 399 + 400 + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.3': 401 + resolution: {integrity: sha512-a4VUQZH7LxGbUJ3qJ/TzQG8HxdHvf+jOnqf7B7oFx1TEBm+j2KNL2zr5SQ7wHkNAcaPevF6gf9tQnVBnC4mD+A==} 402 + engines: {node: ^20.19.0 || >=22.12.0} 403 + cpu: [x64] 404 + os: [win32] 405 + 406 + '@rolldown/pluginutils@1.0.0-rc.3': 407 + resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} 408 + 409 + '@rollup/rollup-android-arm-eabi@4.57.1': 410 + resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} 411 + cpu: [arm] 412 + os: [android] 413 + 414 + '@rollup/rollup-android-arm64@4.57.1': 415 + resolution: {integrity: sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==} 416 + cpu: [arm64] 417 + os: [android] 418 + 419 + '@rollup/rollup-darwin-arm64@4.57.1': 420 + resolution: {integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==} 421 + cpu: [arm64] 422 + os: [darwin] 423 + 424 + '@rollup/rollup-darwin-x64@4.57.1': 425 + resolution: {integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==} 426 + cpu: [x64] 427 + os: [darwin] 428 + 429 + '@rollup/rollup-freebsd-arm64@4.57.1': 430 + resolution: {integrity: sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==} 431 + cpu: [arm64] 432 + os: [freebsd] 433 + 434 + '@rollup/rollup-freebsd-x64@4.57.1': 435 + resolution: {integrity: sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==} 436 + cpu: [x64] 437 + os: [freebsd] 438 + 439 + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': 440 + resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} 441 + cpu: [arm] 442 + os: [linux] 443 + 444 + '@rollup/rollup-linux-arm-musleabihf@4.57.1': 445 + resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} 446 + cpu: [arm] 447 + os: [linux] 448 + 449 + '@rollup/rollup-linux-arm64-gnu@4.57.1': 450 + resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} 451 + cpu: [arm64] 452 + os: [linux] 453 + 454 + '@rollup/rollup-linux-arm64-musl@4.57.1': 455 + resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} 456 + cpu: [arm64] 457 + os: [linux] 458 + 459 + '@rollup/rollup-linux-loong64-gnu@4.57.1': 460 + resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} 461 + cpu: [loong64] 462 + os: [linux] 463 + 464 + '@rollup/rollup-linux-loong64-musl@4.57.1': 465 + resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} 466 + cpu: [loong64] 467 + os: [linux] 468 + 469 + '@rollup/rollup-linux-ppc64-gnu@4.57.1': 470 + resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} 471 + cpu: [ppc64] 472 + os: [linux] 473 + 474 + '@rollup/rollup-linux-ppc64-musl@4.57.1': 475 + resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} 476 + cpu: [ppc64] 477 + os: [linux] 478 + 479 + '@rollup/rollup-linux-riscv64-gnu@4.57.1': 480 + resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} 481 + cpu: [riscv64] 482 + os: [linux] 483 + 484 + '@rollup/rollup-linux-riscv64-musl@4.57.1': 485 + resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} 486 + cpu: [riscv64] 487 + os: [linux] 488 + 489 + '@rollup/rollup-linux-s390x-gnu@4.57.1': 490 + resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} 491 + cpu: [s390x] 492 + os: [linux] 493 + 494 + '@rollup/rollup-linux-x64-gnu@4.57.1': 495 + resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} 496 + cpu: [x64] 497 + os: [linux] 498 + 499 + '@rollup/rollup-linux-x64-musl@4.57.1': 500 + resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} 501 + cpu: [x64] 502 + os: [linux] 503 + 504 + '@rollup/rollup-openbsd-x64@4.57.1': 505 + resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==} 506 + cpu: [x64] 507 + os: [openbsd] 508 + 509 + '@rollup/rollup-openharmony-arm64@4.57.1': 510 + resolution: {integrity: sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==} 511 + cpu: [arm64] 512 + os: [openharmony] 513 + 514 + '@rollup/rollup-win32-arm64-msvc@4.57.1': 515 + resolution: {integrity: sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==} 516 + cpu: [arm64] 517 + os: [win32] 518 + 519 + '@rollup/rollup-win32-ia32-msvc@4.57.1': 520 + resolution: {integrity: sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==} 521 + cpu: [ia32] 522 + os: [win32] 523 + 524 + '@rollup/rollup-win32-x64-gnu@4.57.1': 525 + resolution: {integrity: sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==} 526 + cpu: [x64] 527 + os: [win32] 528 + 529 + '@rollup/rollup-win32-x64-msvc@4.57.1': 530 + resolution: {integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==} 531 + cpu: [x64] 532 + os: [win32] 533 + 534 + '@standard-schema/spec@1.1.0': 535 + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} 536 + 537 + '@tybys/wasm-util@0.10.1': 538 + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} 539 + 540 + '@types/chai@5.2.3': 541 + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} 542 + 543 + '@types/deep-eql@4.0.2': 544 + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} 545 + 546 + '@types/estree@1.0.8': 547 + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} 548 + 549 + '@types/jsesc@2.5.1': 550 + resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} 551 + 552 + '@types/node@25.2.0': 553 + resolution: {integrity: sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==} 554 + 555 + '@vitest/expect@4.0.18': 556 + resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} 557 + 558 + '@vitest/mocker@4.0.18': 559 + resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} 560 + peerDependencies: 561 + msw: ^2.4.9 562 + vite: ^6.0.0 || ^7.0.0-0 563 + peerDependenciesMeta: 564 + msw: 565 + optional: true 566 + vite: 567 + optional: true 568 + 569 + '@vitest/pretty-format@4.0.18': 570 + resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} 571 + 572 + '@vitest/runner@4.0.18': 573 + resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} 574 + 575 + '@vitest/snapshot@4.0.18': 576 + resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} 577 + 578 + '@vitest/spy@4.0.18': 579 + resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} 580 + 581 + '@vitest/ui@4.0.18': 582 + resolution: {integrity: sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==} 583 + peerDependencies: 584 + vitest: 4.0.18 585 + 586 + '@vitest/utils@4.0.18': 587 + resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} 588 + 589 + ansis@4.2.0: 590 + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} 591 + engines: {node: '>=14'} 592 + 593 + assertion-error@2.0.1: 594 + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} 595 + engines: {node: '>=12'} 596 + 597 + ast-kit@3.0.0-beta.1: 598 + resolution: {integrity: sha512-trmleAnZ2PxN/loHWVhhx1qeOHSRXq4TDsBBxq3GqeJitfk3+jTQ+v/C1km/KYq9M7wKqCewMh+/NAvVH7m+bw==} 599 + engines: {node: '>=20.19.0'} 600 + 601 + birpc@4.0.0: 602 + resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} 603 + 604 + cac@6.7.14: 605 + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} 606 + engines: {node: '>=8'} 607 + 608 + chai@6.2.2: 609 + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} 610 + engines: {node: '>=18'} 611 + 612 + defu@6.1.4: 613 + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} 614 + 615 + dts-resolver@2.1.3: 616 + resolution: {integrity: sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==} 617 + engines: {node: '>=20.19.0'} 618 + peerDependencies: 619 + oxc-resolver: '>=11.0.0' 620 + peerDependenciesMeta: 621 + oxc-resolver: 622 + optional: true 623 + 624 + empathic@2.0.0: 625 + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} 626 + engines: {node: '>=14'} 627 + 628 + es-module-lexer@1.7.0: 629 + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} 630 + 631 + esbuild@0.27.2: 632 + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} 633 + engines: {node: '>=18'} 634 + hasBin: true 635 + 636 + estree-walker@3.0.3: 637 + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} 638 + 639 + expect-type@1.3.0: 640 + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} 641 + engines: {node: '>=12.0.0'} 642 + 643 + fdir@6.5.0: 644 + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} 645 + engines: {node: '>=12.0.0'} 646 + peerDependencies: 647 + picomatch: ^3 || ^4 648 + peerDependenciesMeta: 649 + picomatch: 650 + optional: true 651 + 652 + fflate@0.8.2: 653 + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} 654 + 655 + flatted@3.3.3: 656 + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} 657 + 658 + fsevents@2.3.3: 659 + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 660 + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 661 + os: [darwin] 662 + 663 + get-tsconfig@4.13.1: 664 + resolution: {integrity: sha512-EoY1N2xCn44xU6750Sx7OjOIT59FkmstNc3X6y5xpz7D5cBtZRe/3pSlTkDJgqsOk3WwZPkWfonhhUJfttQo3w==} 665 + 666 + hookable@6.0.1: 667 + resolution: {integrity: sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw==} 668 + 669 + import-without-cache@0.2.5: 670 + resolution: {integrity: sha512-B6Lc2s6yApwnD2/pMzFh/d5AVjdsDXjgkeJ766FmFuJELIGHNycKRj+l3A39yZPM4CchqNCB4RITEAYB1KUM6A==} 671 + engines: {node: '>=20.19.0'} 672 + 673 + jsesc@3.1.0: 674 + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} 675 + engines: {node: '>=6'} 676 + hasBin: true 677 + 678 + magic-string@0.30.21: 679 + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 680 + 681 + mrmime@2.0.1: 682 + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} 683 + engines: {node: '>=10'} 684 + 685 + nanoid@3.3.11: 686 + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} 687 + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 688 + hasBin: true 689 + 690 + obug@2.1.1: 691 + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} 692 + 693 + oxfmt@0.28.0: 694 + resolution: {integrity: sha512-3+hhBqPE6Kp22KfJmnstrZbl+KdOVSEu1V0ABaFIg1rYLtrMgrupx9znnHgHLqKxAVHebjTdiCJDk30CXOt6cw==} 695 + engines: {node: ^20.19.0 || >=22.12.0} 696 + hasBin: true 697 + 698 + oxlint@1.43.0: 699 + resolution: {integrity: sha512-xiqTCsKZch+R61DPCjyqUVP2MhkQlRRYxLRBeBDi+dtQJ90MOgdcjIktvDCgXz0bgtx94EQzHEndsizZjMX2OA==} 700 + engines: {node: ^20.19.0 || >=22.12.0} 701 + hasBin: true 702 + peerDependencies: 703 + oxlint-tsgolint: '>=0.11.2' 704 + peerDependenciesMeta: 705 + oxlint-tsgolint: 706 + optional: true 707 + 708 + pathe@2.0.3: 709 + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} 710 + 711 + picocolors@1.1.1: 712 + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 713 + 714 + picomatch@4.0.3: 715 + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} 716 + engines: {node: '>=12'} 717 + 718 + postcss@8.5.6: 719 + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} 720 + engines: {node: ^10 || ^12 || >=14} 721 + 722 + quansync@1.0.0: 723 + resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} 724 + 725 + resolve-pkg-maps@1.0.0: 726 + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} 727 + 728 + rolldown-plugin-dts@0.22.1: 729 + resolution: {integrity: sha512-5E0AiM5RSQhU6cjtkDFWH6laW4IrMu0j1Mo8x04Xo1ALHmaRMs9/7zej7P3RrryVHW/DdZAp85MA7Be55p0iUw==} 730 + engines: {node: '>=20.19.0'} 731 + peerDependencies: 732 + '@ts-macro/tsc': ^0.3.6 733 + '@typescript/native-preview': '>=7.0.0-dev.20250601.1' 734 + rolldown: ^1.0.0-rc.3 735 + typescript: ^5.0.0 736 + vue-tsc: ~3.2.0 737 + peerDependenciesMeta: 738 + '@ts-macro/tsc': 739 + optional: true 740 + '@typescript/native-preview': 741 + optional: true 742 + typescript: 743 + optional: true 744 + vue-tsc: 745 + optional: true 746 + 747 + rolldown@1.0.0-rc.3: 748 + resolution: {integrity: sha512-Po/YZECDOqVXjIXrtC5h++a5NLvKAQNrd9ggrIG3sbDfGO5BqTUsrI6l8zdniKRp3r5Tp/2JTrXqx4GIguFCMw==} 749 + engines: {node: ^20.19.0 || >=22.12.0} 750 + hasBin: true 751 + 752 + rollup@4.57.1: 753 + resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==} 754 + engines: {node: '>=18.0.0', npm: '>=8.0.0'} 755 + hasBin: true 756 + 757 + semver@7.7.3: 758 + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} 759 + engines: {node: '>=10'} 760 + hasBin: true 761 + 762 + siginfo@2.0.0: 763 + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} 764 + 765 + sirv@3.0.2: 766 + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} 767 + engines: {node: '>=18'} 768 + 769 + source-map-js@1.2.1: 770 + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 771 + engines: {node: '>=0.10.0'} 772 + 773 + stackback@0.0.2: 774 + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} 775 + 776 + std-env@3.10.0: 777 + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} 778 + 779 + tinybench@2.9.0: 780 + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} 781 + 782 + tinyexec@1.0.2: 783 + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} 784 + engines: {node: '>=18'} 785 + 786 + tinyglobby@0.2.15: 787 + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} 788 + engines: {node: '>=12.0.0'} 789 + 790 + tinypool@2.1.0: 791 + resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} 792 + engines: {node: ^20.0.0 || >=22.0.0} 793 + 794 + tinyrainbow@3.0.3: 795 + resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} 796 + engines: {node: '>=14.0.0'} 797 + 798 + totalist@3.0.1: 799 + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} 800 + engines: {node: '>=6'} 801 + 802 + tree-kill@1.2.2: 803 + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} 804 + hasBin: true 805 + 806 + tsdown@0.20.3: 807 + resolution: {integrity: sha512-qWOUXSbe4jN8JZEgrkc/uhJpC8VN2QpNu3eZkBWwNuTEjc/Ik1kcc54ycfcQ5QPRHeu9OQXaLfCI3o7pEJgB2w==} 808 + engines: {node: '>=20.19.0'} 809 + hasBin: true 810 + peerDependencies: 811 + '@arethetypeswrong/core': ^0.18.1 812 + '@vitejs/devtools': '*' 813 + publint: ^0.3.0 814 + typescript: ^5.0.0 815 + unplugin-lightningcss: ^0.4.0 816 + unplugin-unused: ^0.5.0 817 + peerDependenciesMeta: 818 + '@arethetypeswrong/core': 819 + optional: true 820 + '@vitejs/devtools': 821 + optional: true 822 + publint: 823 + optional: true 824 + typescript: 825 + optional: true 826 + unplugin-lightningcss: 827 + optional: true 828 + unplugin-unused: 829 + optional: true 830 + 831 + tslib@2.8.1: 832 + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 833 + 834 + typescript@5.9.3: 835 + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} 836 + engines: {node: '>=14.17'} 837 + hasBin: true 838 + 839 + unconfig-core@7.4.2: 840 + resolution: {integrity: sha512-VgPCvLWugINbXvMQDf8Jh0mlbvNjNC6eSUziHsBCMpxR05OPrNrvDnyatdMjRgcHaaNsCqz+wjNXxNw1kRLHUg==} 841 + 842 + undici-types@7.16.0: 843 + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} 844 + 845 + unrun@0.2.27: 846 + resolution: {integrity: sha512-Mmur1UJpIbfxasLOhPRvox/QS4xBiDii71hMP7smfRthGcwFL2OAmYRgduLANOAU4LUkvVamuP+02U+c90jlrw==} 847 + engines: {node: '>=20.19.0'} 848 + hasBin: true 849 + peerDependencies: 850 + synckit: ^0.11.11 851 + peerDependenciesMeta: 852 + synckit: 853 + optional: true 854 + 855 + vite@7.3.1: 856 + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} 857 + engines: {node: ^20.19.0 || >=22.12.0} 858 + hasBin: true 859 + peerDependencies: 860 + '@types/node': ^20.19.0 || >=22.12.0 861 + jiti: '>=1.21.0' 862 + less: ^4.0.0 863 + lightningcss: ^1.21.0 864 + sass: ^1.70.0 865 + sass-embedded: ^1.70.0 866 + stylus: '>=0.54.8' 867 + sugarss: ^5.0.0 868 + terser: ^5.16.0 869 + tsx: ^4.8.1 870 + yaml: ^2.4.2 871 + peerDependenciesMeta: 872 + '@types/node': 873 + optional: true 874 + jiti: 875 + optional: true 876 + less: 877 + optional: true 878 + lightningcss: 879 + optional: true 880 + sass: 881 + optional: true 882 + sass-embedded: 883 + optional: true 884 + stylus: 885 + optional: true 886 + sugarss: 887 + optional: true 888 + terser: 889 + optional: true 890 + tsx: 891 + optional: true 892 + yaml: 893 + optional: true 894 + 895 + vitest@4.0.18: 896 + resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} 897 + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} 898 + hasBin: true 899 + peerDependencies: 900 + '@edge-runtime/vm': '*' 901 + '@opentelemetry/api': ^1.9.0 902 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 903 + '@vitest/browser-playwright': 4.0.18 904 + '@vitest/browser-preview': 4.0.18 905 + '@vitest/browser-webdriverio': 4.0.18 906 + '@vitest/ui': 4.0.18 907 + happy-dom: '*' 908 + jsdom: '*' 909 + peerDependenciesMeta: 910 + '@edge-runtime/vm': 911 + optional: true 912 + '@opentelemetry/api': 913 + optional: true 914 + '@types/node': 915 + optional: true 916 + '@vitest/browser-playwright': 917 + optional: true 918 + '@vitest/browser-preview': 919 + optional: true 920 + '@vitest/browser-webdriverio': 921 + optional: true 922 + '@vitest/ui': 923 + optional: true 924 + happy-dom: 925 + optional: true 926 + jsdom: 927 + optional: true 928 + 929 + why-is-node-running@2.3.0: 930 + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} 931 + engines: {node: '>=8'} 932 + hasBin: true 933 + 934 + snapshots: 935 + 936 + '@babel/generator@8.0.0-rc.1': 937 + dependencies: 938 + '@babel/parser': 8.0.0-rc.1 939 + '@babel/types': 8.0.0-rc.1 940 + '@jridgewell/gen-mapping': 0.3.13 941 + '@jridgewell/trace-mapping': 0.3.31 942 + '@types/jsesc': 2.5.1 943 + jsesc: 3.1.0 944 + 945 + '@babel/helper-string-parser@8.0.0-rc.1': {} 946 + 947 + '@babel/helper-validator-identifier@8.0.0-rc.1': {} 948 + 949 + '@babel/parser@8.0.0-rc.1': 950 + dependencies: 951 + '@babel/types': 8.0.0-rc.1 952 + 953 + '@babel/types@8.0.0-rc.1': 954 + dependencies: 955 + '@babel/helper-string-parser': 8.0.0-rc.1 956 + '@babel/helper-validator-identifier': 8.0.0-rc.1 957 + 958 + '@emnapi/core@1.8.1': 959 + dependencies: 960 + '@emnapi/wasi-threads': 1.1.0 961 + tslib: 2.8.1 962 + optional: true 963 + 964 + '@emnapi/runtime@1.8.1': 965 + dependencies: 966 + tslib: 2.8.1 967 + optional: true 968 + 969 + '@emnapi/wasi-threads@1.1.0': 970 + dependencies: 971 + tslib: 2.8.1 972 + optional: true 973 + 974 + '@esbuild/aix-ppc64@0.27.2': 975 + optional: true 976 + 977 + '@esbuild/android-arm64@0.27.2': 978 + optional: true 979 + 980 + '@esbuild/android-arm@0.27.2': 981 + optional: true 982 + 983 + '@esbuild/android-x64@0.27.2': 984 + optional: true 985 + 986 + '@esbuild/darwin-arm64@0.27.2': 987 + optional: true 988 + 989 + '@esbuild/darwin-x64@0.27.2': 990 + optional: true 991 + 992 + '@esbuild/freebsd-arm64@0.27.2': 993 + optional: true 994 + 995 + '@esbuild/freebsd-x64@0.27.2': 996 + optional: true 997 + 998 + '@esbuild/linux-arm64@0.27.2': 999 + optional: true 1000 + 1001 + '@esbuild/linux-arm@0.27.2': 1002 + optional: true 1003 + 1004 + '@esbuild/linux-ia32@0.27.2': 1005 + optional: true 1006 + 1007 + '@esbuild/linux-loong64@0.27.2': 1008 + optional: true 1009 + 1010 + '@esbuild/linux-mips64el@0.27.2': 1011 + optional: true 1012 + 1013 + '@esbuild/linux-ppc64@0.27.2': 1014 + optional: true 1015 + 1016 + '@esbuild/linux-riscv64@0.27.2': 1017 + optional: true 1018 + 1019 + '@esbuild/linux-s390x@0.27.2': 1020 + optional: true 1021 + 1022 + '@esbuild/linux-x64@0.27.2': 1023 + optional: true 1024 + 1025 + '@esbuild/netbsd-arm64@0.27.2': 1026 + optional: true 1027 + 1028 + '@esbuild/netbsd-x64@0.27.2': 1029 + optional: true 1030 + 1031 + '@esbuild/openbsd-arm64@0.27.2': 1032 + optional: true 1033 + 1034 + '@esbuild/openbsd-x64@0.27.2': 1035 + optional: true 1036 + 1037 + '@esbuild/openharmony-arm64@0.27.2': 1038 + optional: true 1039 + 1040 + '@esbuild/sunos-x64@0.27.2': 1041 + optional: true 1042 + 1043 + '@esbuild/win32-arm64@0.27.2': 1044 + optional: true 1045 + 1046 + '@esbuild/win32-ia32@0.27.2': 1047 + optional: true 1048 + 1049 + '@esbuild/win32-x64@0.27.2': 1050 + optional: true 1051 + 1052 + '@jridgewell/gen-mapping@0.3.13': 1053 + dependencies: 1054 + '@jridgewell/sourcemap-codec': 1.5.5 1055 + '@jridgewell/trace-mapping': 0.3.31 1056 + 1057 + '@jridgewell/resolve-uri@3.1.2': {} 1058 + 1059 + '@jridgewell/sourcemap-codec@1.5.5': {} 1060 + 1061 + '@jridgewell/trace-mapping@0.3.31': 1062 + dependencies: 1063 + '@jridgewell/resolve-uri': 3.1.2 1064 + '@jridgewell/sourcemap-codec': 1.5.5 1065 + 1066 + '@napi-rs/wasm-runtime@1.1.1': 1067 + dependencies: 1068 + '@emnapi/core': 1.8.1 1069 + '@emnapi/runtime': 1.8.1 1070 + '@tybys/wasm-util': 0.10.1 1071 + optional: true 1072 + 1073 + '@oxc-project/types@0.112.0': {} 1074 + 1075 + '@oxfmt/darwin-arm64@0.28.0': 1076 + optional: true 1077 + 1078 + '@oxfmt/darwin-x64@0.28.0': 1079 + optional: true 1080 + 1081 + '@oxfmt/linux-arm64-gnu@0.28.0': 1082 + optional: true 1083 + 1084 + '@oxfmt/linux-arm64-musl@0.28.0': 1085 + optional: true 1086 + 1087 + '@oxfmt/linux-x64-gnu@0.28.0': 1088 + optional: true 1089 + 1090 + '@oxfmt/linux-x64-musl@0.28.0': 1091 + optional: true 1092 + 1093 + '@oxfmt/win32-arm64@0.28.0': 1094 + optional: true 1095 + 1096 + '@oxfmt/win32-x64@0.28.0': 1097 + optional: true 1098 + 1099 + '@oxlint/darwin-arm64@1.43.0': 1100 + optional: true 1101 + 1102 + '@oxlint/darwin-x64@1.43.0': 1103 + optional: true 1104 + 1105 + '@oxlint/linux-arm64-gnu@1.43.0': 1106 + optional: true 1107 + 1108 + '@oxlint/linux-arm64-musl@1.43.0': 1109 + optional: true 1110 + 1111 + '@oxlint/linux-x64-gnu@1.43.0': 1112 + optional: true 1113 + 1114 + '@oxlint/linux-x64-musl@1.43.0': 1115 + optional: true 1116 + 1117 + '@oxlint/win32-arm64@1.43.0': 1118 + optional: true 1119 + 1120 + '@oxlint/win32-x64@1.43.0': 1121 + optional: true 1122 + 1123 + '@polka/url@1.0.0-next.29': {} 1124 + 1125 + '@quansync/fs@1.0.0': 1126 + dependencies: 1127 + quansync: 1.0.0 1128 + 1129 + '@rolldown/binding-android-arm64@1.0.0-rc.3': 1130 + optional: true 1131 + 1132 + '@rolldown/binding-darwin-arm64@1.0.0-rc.3': 1133 + optional: true 1134 + 1135 + '@rolldown/binding-darwin-x64@1.0.0-rc.3': 1136 + optional: true 1137 + 1138 + '@rolldown/binding-freebsd-x64@1.0.0-rc.3': 1139 + optional: true 1140 + 1141 + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.3': 1142 + optional: true 1143 + 1144 + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.3': 1145 + optional: true 1146 + 1147 + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.3': 1148 + optional: true 1149 + 1150 + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.3': 1151 + optional: true 1152 + 1153 + '@rolldown/binding-linux-x64-musl@1.0.0-rc.3': 1154 + optional: true 1155 + 1156 + '@rolldown/binding-openharmony-arm64@1.0.0-rc.3': 1157 + optional: true 1158 + 1159 + '@rolldown/binding-wasm32-wasi@1.0.0-rc.3': 1160 + dependencies: 1161 + '@napi-rs/wasm-runtime': 1.1.1 1162 + optional: true 1163 + 1164 + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.3': 1165 + optional: true 1166 + 1167 + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.3': 1168 + optional: true 1169 + 1170 + '@rolldown/pluginutils@1.0.0-rc.3': {} 1171 + 1172 + '@rollup/rollup-android-arm-eabi@4.57.1': 1173 + optional: true 1174 + 1175 + '@rollup/rollup-android-arm64@4.57.1': 1176 + optional: true 1177 + 1178 + '@rollup/rollup-darwin-arm64@4.57.1': 1179 + optional: true 1180 + 1181 + '@rollup/rollup-darwin-x64@4.57.1': 1182 + optional: true 1183 + 1184 + '@rollup/rollup-freebsd-arm64@4.57.1': 1185 + optional: true 1186 + 1187 + '@rollup/rollup-freebsd-x64@4.57.1': 1188 + optional: true 1189 + 1190 + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': 1191 + optional: true 1192 + 1193 + '@rollup/rollup-linux-arm-musleabihf@4.57.1': 1194 + optional: true 1195 + 1196 + '@rollup/rollup-linux-arm64-gnu@4.57.1': 1197 + optional: true 1198 + 1199 + '@rollup/rollup-linux-arm64-musl@4.57.1': 1200 + optional: true 1201 + 1202 + '@rollup/rollup-linux-loong64-gnu@4.57.1': 1203 + optional: true 1204 + 1205 + '@rollup/rollup-linux-loong64-musl@4.57.1': 1206 + optional: true 1207 + 1208 + '@rollup/rollup-linux-ppc64-gnu@4.57.1': 1209 + optional: true 1210 + 1211 + '@rollup/rollup-linux-ppc64-musl@4.57.1': 1212 + optional: true 1213 + 1214 + '@rollup/rollup-linux-riscv64-gnu@4.57.1': 1215 + optional: true 1216 + 1217 + '@rollup/rollup-linux-riscv64-musl@4.57.1': 1218 + optional: true 1219 + 1220 + '@rollup/rollup-linux-s390x-gnu@4.57.1': 1221 + optional: true 1222 + 1223 + '@rollup/rollup-linux-x64-gnu@4.57.1': 1224 + optional: true 1225 + 1226 + '@rollup/rollup-linux-x64-musl@4.57.1': 1227 + optional: true 1228 + 1229 + '@rollup/rollup-openbsd-x64@4.57.1': 1230 + optional: true 1231 + 1232 + '@rollup/rollup-openharmony-arm64@4.57.1': 1233 + optional: true 1234 + 1235 + '@rollup/rollup-win32-arm64-msvc@4.57.1': 1236 + optional: true 1237 + 1238 + '@rollup/rollup-win32-ia32-msvc@4.57.1': 1239 + optional: true 1240 + 1241 + '@rollup/rollup-win32-x64-gnu@4.57.1': 1242 + optional: true 1243 + 1244 + '@rollup/rollup-win32-x64-msvc@4.57.1': 1245 + optional: true 1246 + 1247 + '@standard-schema/spec@1.1.0': {} 1248 + 1249 + '@tybys/wasm-util@0.10.1': 1250 + dependencies: 1251 + tslib: 2.8.1 1252 + optional: true 1253 + 1254 + '@types/chai@5.2.3': 1255 + dependencies: 1256 + '@types/deep-eql': 4.0.2 1257 + assertion-error: 2.0.1 1258 + 1259 + '@types/deep-eql@4.0.2': {} 1260 + 1261 + '@types/estree@1.0.8': {} 1262 + 1263 + '@types/jsesc@2.5.1': {} 1264 + 1265 + '@types/node@25.2.0': 1266 + dependencies: 1267 + undici-types: 7.16.0 1268 + 1269 + '@vitest/expect@4.0.18': 1270 + dependencies: 1271 + '@standard-schema/spec': 1.1.0 1272 + '@types/chai': 5.2.3 1273 + '@vitest/spy': 4.0.18 1274 + '@vitest/utils': 4.0.18 1275 + chai: 6.2.2 1276 + tinyrainbow: 3.0.3 1277 + 1278 + '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.2.0))': 1279 + dependencies: 1280 + '@vitest/spy': 4.0.18 1281 + estree-walker: 3.0.3 1282 + magic-string: 0.30.21 1283 + optionalDependencies: 1284 + vite: 7.3.1(@types/node@25.2.0) 1285 + 1286 + '@vitest/pretty-format@4.0.18': 1287 + dependencies: 1288 + tinyrainbow: 3.0.3 1289 + 1290 + '@vitest/runner@4.0.18': 1291 + dependencies: 1292 + '@vitest/utils': 4.0.18 1293 + pathe: 2.0.3 1294 + 1295 + '@vitest/snapshot@4.0.18': 1296 + dependencies: 1297 + '@vitest/pretty-format': 4.0.18 1298 + magic-string: 0.30.21 1299 + pathe: 2.0.3 1300 + 1301 + '@vitest/spy@4.0.18': {} 1302 + 1303 + '@vitest/ui@4.0.18(vitest@4.0.18)': 1304 + dependencies: 1305 + '@vitest/utils': 4.0.18 1306 + fflate: 0.8.2 1307 + flatted: 3.3.3 1308 + pathe: 2.0.3 1309 + sirv: 3.0.2 1310 + tinyglobby: 0.2.15 1311 + tinyrainbow: 3.0.3 1312 + vitest: 4.0.18(@types/node@25.2.0)(@vitest/ui@4.0.18) 1313 + 1314 + '@vitest/utils@4.0.18': 1315 + dependencies: 1316 + '@vitest/pretty-format': 4.0.18 1317 + tinyrainbow: 3.0.3 1318 + 1319 + ansis@4.2.0: {} 1320 + 1321 + assertion-error@2.0.1: {} 1322 + 1323 + ast-kit@3.0.0-beta.1: 1324 + dependencies: 1325 + '@babel/parser': 8.0.0-rc.1 1326 + estree-walker: 3.0.3 1327 + pathe: 2.0.3 1328 + 1329 + birpc@4.0.0: {} 1330 + 1331 + cac@6.7.14: {} 1332 + 1333 + chai@6.2.2: {} 1334 + 1335 + defu@6.1.4: {} 1336 + 1337 + dts-resolver@2.1.3: {} 1338 + 1339 + empathic@2.0.0: {} 1340 + 1341 + es-module-lexer@1.7.0: {} 1342 + 1343 + esbuild@0.27.2: 1344 + optionalDependencies: 1345 + '@esbuild/aix-ppc64': 0.27.2 1346 + '@esbuild/android-arm': 0.27.2 1347 + '@esbuild/android-arm64': 0.27.2 1348 + '@esbuild/android-x64': 0.27.2 1349 + '@esbuild/darwin-arm64': 0.27.2 1350 + '@esbuild/darwin-x64': 0.27.2 1351 + '@esbuild/freebsd-arm64': 0.27.2 1352 + '@esbuild/freebsd-x64': 0.27.2 1353 + '@esbuild/linux-arm': 0.27.2 1354 + '@esbuild/linux-arm64': 0.27.2 1355 + '@esbuild/linux-ia32': 0.27.2 1356 + '@esbuild/linux-loong64': 0.27.2 1357 + '@esbuild/linux-mips64el': 0.27.2 1358 + '@esbuild/linux-ppc64': 0.27.2 1359 + '@esbuild/linux-riscv64': 0.27.2 1360 + '@esbuild/linux-s390x': 0.27.2 1361 + '@esbuild/linux-x64': 0.27.2 1362 + '@esbuild/netbsd-arm64': 0.27.2 1363 + '@esbuild/netbsd-x64': 0.27.2 1364 + '@esbuild/openbsd-arm64': 0.27.2 1365 + '@esbuild/openbsd-x64': 0.27.2 1366 + '@esbuild/openharmony-arm64': 0.27.2 1367 + '@esbuild/sunos-x64': 0.27.2 1368 + '@esbuild/win32-arm64': 0.27.2 1369 + '@esbuild/win32-ia32': 0.27.2 1370 + '@esbuild/win32-x64': 0.27.2 1371 + 1372 + estree-walker@3.0.3: 1373 + dependencies: 1374 + '@types/estree': 1.0.8 1375 + 1376 + expect-type@1.3.0: {} 1377 + 1378 + fdir@6.5.0(picomatch@4.0.3): 1379 + optionalDependencies: 1380 + picomatch: 4.0.3 1381 + 1382 + fflate@0.8.2: {} 1383 + 1384 + flatted@3.3.3: {} 1385 + 1386 + fsevents@2.3.3: 1387 + optional: true 1388 + 1389 + get-tsconfig@4.13.1: 1390 + dependencies: 1391 + resolve-pkg-maps: 1.0.0 1392 + 1393 + hookable@6.0.1: {} 1394 + 1395 + import-without-cache@0.2.5: {} 1396 + 1397 + jsesc@3.1.0: {} 1398 + 1399 + magic-string@0.30.21: 1400 + dependencies: 1401 + '@jridgewell/sourcemap-codec': 1.5.5 1402 + 1403 + mrmime@2.0.1: {} 1404 + 1405 + nanoid@3.3.11: {} 1406 + 1407 + obug@2.1.1: {} 1408 + 1409 + oxfmt@0.28.0: 1410 + dependencies: 1411 + tinypool: 2.1.0 1412 + optionalDependencies: 1413 + '@oxfmt/darwin-arm64': 0.28.0 1414 + '@oxfmt/darwin-x64': 0.28.0 1415 + '@oxfmt/linux-arm64-gnu': 0.28.0 1416 + '@oxfmt/linux-arm64-musl': 0.28.0 1417 + '@oxfmt/linux-x64-gnu': 0.28.0 1418 + '@oxfmt/linux-x64-musl': 0.28.0 1419 + '@oxfmt/win32-arm64': 0.28.0 1420 + '@oxfmt/win32-x64': 0.28.0 1421 + 1422 + oxlint@1.43.0: 1423 + optionalDependencies: 1424 + '@oxlint/darwin-arm64': 1.43.0 1425 + '@oxlint/darwin-x64': 1.43.0 1426 + '@oxlint/linux-arm64-gnu': 1.43.0 1427 + '@oxlint/linux-arm64-musl': 1.43.0 1428 + '@oxlint/linux-x64-gnu': 1.43.0 1429 + '@oxlint/linux-x64-musl': 1.43.0 1430 + '@oxlint/win32-arm64': 1.43.0 1431 + '@oxlint/win32-x64': 1.43.0 1432 + 1433 + pathe@2.0.3: {} 1434 + 1435 + picocolors@1.1.1: {} 1436 + 1437 + picomatch@4.0.3: {} 1438 + 1439 + postcss@8.5.6: 1440 + dependencies: 1441 + nanoid: 3.3.11 1442 + picocolors: 1.1.1 1443 + source-map-js: 1.2.1 1444 + 1445 + quansync@1.0.0: {} 1446 + 1447 + resolve-pkg-maps@1.0.0: {} 1448 + 1449 + rolldown-plugin-dts@0.22.1(rolldown@1.0.0-rc.3)(typescript@5.9.3): 1450 + dependencies: 1451 + '@babel/generator': 8.0.0-rc.1 1452 + '@babel/helper-validator-identifier': 8.0.0-rc.1 1453 + '@babel/parser': 8.0.0-rc.1 1454 + '@babel/types': 8.0.0-rc.1 1455 + ast-kit: 3.0.0-beta.1 1456 + birpc: 4.0.0 1457 + dts-resolver: 2.1.3 1458 + get-tsconfig: 4.13.1 1459 + obug: 2.1.1 1460 + rolldown: 1.0.0-rc.3 1461 + optionalDependencies: 1462 + typescript: 5.9.3 1463 + transitivePeerDependencies: 1464 + - oxc-resolver 1465 + 1466 + rolldown@1.0.0-rc.3: 1467 + dependencies: 1468 + '@oxc-project/types': 0.112.0 1469 + '@rolldown/pluginutils': 1.0.0-rc.3 1470 + optionalDependencies: 1471 + '@rolldown/binding-android-arm64': 1.0.0-rc.3 1472 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.3 1473 + '@rolldown/binding-darwin-x64': 1.0.0-rc.3 1474 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.3 1475 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.3 1476 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.3 1477 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.3 1478 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.3 1479 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.3 1480 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.3 1481 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.3 1482 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.3 1483 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.3 1484 + 1485 + rollup@4.57.1: 1486 + dependencies: 1487 + '@types/estree': 1.0.8 1488 + optionalDependencies: 1489 + '@rollup/rollup-android-arm-eabi': 4.57.1 1490 + '@rollup/rollup-android-arm64': 4.57.1 1491 + '@rollup/rollup-darwin-arm64': 4.57.1 1492 + '@rollup/rollup-darwin-x64': 4.57.1 1493 + '@rollup/rollup-freebsd-arm64': 4.57.1 1494 + '@rollup/rollup-freebsd-x64': 4.57.1 1495 + '@rollup/rollup-linux-arm-gnueabihf': 4.57.1 1496 + '@rollup/rollup-linux-arm-musleabihf': 4.57.1 1497 + '@rollup/rollup-linux-arm64-gnu': 4.57.1 1498 + '@rollup/rollup-linux-arm64-musl': 4.57.1 1499 + '@rollup/rollup-linux-loong64-gnu': 4.57.1 1500 + '@rollup/rollup-linux-loong64-musl': 4.57.1 1501 + '@rollup/rollup-linux-ppc64-gnu': 4.57.1 1502 + '@rollup/rollup-linux-ppc64-musl': 4.57.1 1503 + '@rollup/rollup-linux-riscv64-gnu': 4.57.1 1504 + '@rollup/rollup-linux-riscv64-musl': 4.57.1 1505 + '@rollup/rollup-linux-s390x-gnu': 4.57.1 1506 + '@rollup/rollup-linux-x64-gnu': 4.57.1 1507 + '@rollup/rollup-linux-x64-musl': 4.57.1 1508 + '@rollup/rollup-openbsd-x64': 4.57.1 1509 + '@rollup/rollup-openharmony-arm64': 4.57.1 1510 + '@rollup/rollup-win32-arm64-msvc': 4.57.1 1511 + '@rollup/rollup-win32-ia32-msvc': 4.57.1 1512 + '@rollup/rollup-win32-x64-gnu': 4.57.1 1513 + '@rollup/rollup-win32-x64-msvc': 4.57.1 1514 + fsevents: 2.3.3 1515 + 1516 + semver@7.7.3: {} 1517 + 1518 + siginfo@2.0.0: {} 1519 + 1520 + sirv@3.0.2: 1521 + dependencies: 1522 + '@polka/url': 1.0.0-next.29 1523 + mrmime: 2.0.1 1524 + totalist: 3.0.1 1525 + 1526 + source-map-js@1.2.1: {} 1527 + 1528 + stackback@0.0.2: {} 1529 + 1530 + std-env@3.10.0: {} 1531 + 1532 + tinybench@2.9.0: {} 1533 + 1534 + tinyexec@1.0.2: {} 1535 + 1536 + tinyglobby@0.2.15: 1537 + dependencies: 1538 + fdir: 6.5.0(picomatch@4.0.3) 1539 + picomatch: 4.0.3 1540 + 1541 + tinypool@2.1.0: {} 1542 + 1543 + tinyrainbow@3.0.3: {} 1544 + 1545 + totalist@3.0.1: {} 1546 + 1547 + tree-kill@1.2.2: {} 1548 + 1549 + tsdown@0.20.3(typescript@5.9.3): 1550 + dependencies: 1551 + ansis: 4.2.0 1552 + cac: 6.7.14 1553 + defu: 6.1.4 1554 + empathic: 2.0.0 1555 + hookable: 6.0.1 1556 + import-without-cache: 0.2.5 1557 + obug: 2.1.1 1558 + picomatch: 4.0.3 1559 + rolldown: 1.0.0-rc.3 1560 + rolldown-plugin-dts: 0.22.1(rolldown@1.0.0-rc.3)(typescript@5.9.3) 1561 + semver: 7.7.3 1562 + tinyexec: 1.0.2 1563 + tinyglobby: 0.2.15 1564 + tree-kill: 1.2.2 1565 + unconfig-core: 7.4.2 1566 + unrun: 0.2.27 1567 + optionalDependencies: 1568 + typescript: 5.9.3 1569 + transitivePeerDependencies: 1570 + - '@ts-macro/tsc' 1571 + - '@typescript/native-preview' 1572 + - oxc-resolver 1573 + - synckit 1574 + - vue-tsc 1575 + 1576 + tslib@2.8.1: 1577 + optional: true 1578 + 1579 + typescript@5.9.3: {} 1580 + 1581 + unconfig-core@7.4.2: 1582 + dependencies: 1583 + '@quansync/fs': 1.0.0 1584 + quansync: 1.0.0 1585 + 1586 + undici-types@7.16.0: {} 1587 + 1588 + unrun@0.2.27: 1589 + dependencies: 1590 + rolldown: 1.0.0-rc.3 1591 + 1592 + vite@7.3.1(@types/node@25.2.0): 1593 + dependencies: 1594 + esbuild: 0.27.2 1595 + fdir: 6.5.0(picomatch@4.0.3) 1596 + picomatch: 4.0.3 1597 + postcss: 8.5.6 1598 + rollup: 4.57.1 1599 + tinyglobby: 0.2.15 1600 + optionalDependencies: 1601 + '@types/node': 25.2.0 1602 + fsevents: 2.3.3 1603 + 1604 + vitest@4.0.18(@types/node@25.2.0)(@vitest/ui@4.0.18): 1605 + dependencies: 1606 + '@vitest/expect': 4.0.18 1607 + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.2.0)) 1608 + '@vitest/pretty-format': 4.0.18 1609 + '@vitest/runner': 4.0.18 1610 + '@vitest/snapshot': 4.0.18 1611 + '@vitest/spy': 4.0.18 1612 + '@vitest/utils': 4.0.18 1613 + es-module-lexer: 1.7.0 1614 + expect-type: 1.3.0 1615 + magic-string: 0.30.21 1616 + obug: 2.1.1 1617 + pathe: 2.0.3 1618 + picomatch: 4.0.3 1619 + std-env: 3.10.0 1620 + tinybench: 2.9.0 1621 + tinyexec: 1.0.2 1622 + tinyglobby: 0.2.15 1623 + tinyrainbow: 3.0.3 1624 + vite: 7.3.1(@types/node@25.2.0) 1625 + why-is-node-running: 2.3.0 1626 + optionalDependencies: 1627 + '@types/node': 25.2.0 1628 + '@vitest/ui': 4.0.18(vitest@4.0.18) 1629 + transitivePeerDependencies: 1630 + - jiti 1631 + - less 1632 + - lightningcss 1633 + - msw 1634 + - sass 1635 + - sass-embedded 1636 + - stylus 1637 + - sugarss 1638 + - terser 1639 + - tsx 1640 + - yaml 1641 + 1642 + why-is-node-running@2.3.0: 1643 + dependencies: 1644 + siginfo: 2.0.0 1645 + stackback: 0.0.2
+8
src/index.ts
··· 1 + /** 2 + * cron-fast - Lightweight, fast cron parser with timezone support 3 + * @packageDocumentation 4 + */ 5 + 6 + export { nextRun, previousRun, nextRuns, isMatch } from "./scheduler.js"; 7 + export { parse, isValid } from "./parser.js"; 8 + export type { CronOptions, ParsedCron } from "./types.js";
+93
src/matcher.ts
··· 1 + import type { ParsedCron } from "./types.js"; 2 + 3 + /** 4 + * Check if a date matches the cron expression 5 + */ 6 + export function matches(parsed: ParsedCron, date: Date): boolean { 7 + const minute = date.getUTCMinutes(); 8 + const hour = date.getUTCHours(); 9 + const day = date.getUTCDate(); 10 + const month = date.getUTCMonth(); // 0-indexed (0 = Jan, 11 = Dec) 11 + const weekday = date.getUTCDay(); 12 + 13 + // Check if all fields match 14 + return ( 15 + parsed.minute.includes(minute) && 16 + parsed.hour.includes(hour) && 17 + parsed.month.includes(month) && 18 + matchesDayOrWeekday(parsed, day, weekday) 19 + ); 20 + } 21 + 22 + /** 23 + * Day-of-month and day-of-week use OR logic by default 24 + * If both are restricted (not *), match either one 25 + */ 26 + function matchesDayOrWeekday(parsed: ParsedCron, day: number, weekday: number): boolean { 27 + const dayMatches = parsed.day.includes(day); 28 + const weekdayMatches = parsed.weekday.includes(weekday); 29 + 30 + // If both are wildcards (all values), both match 31 + const dayIsWildcard = parsed.day.length === 31; 32 + const weekdayIsWildcard = parsed.weekday.length === 7; 33 + 34 + // If both are restricted, use OR logic (standard cron behavior) 35 + if (!dayIsWildcard && !weekdayIsWildcard) { 36 + return dayMatches || weekdayMatches; 37 + } 38 + 39 + // If only one is restricted, it must match 40 + if (!dayIsWildcard) { 41 + return dayMatches; 42 + } 43 + if (!weekdayIsWildcard) { 44 + return weekdayMatches; 45 + } 46 + 47 + // Both wildcards, always matches 48 + return true; 49 + } 50 + 51 + /** 52 + * Find the next value in a sorted array that is >= target 53 + * Returns null if no such value exists 54 + * 55 + * @param values - MUST be sorted in ascending order 56 + * @param target - The minimum value to find 57 + */ 58 + export function findNext(values: number[], target: number): number | null { 59 + for (const value of values) { 60 + if (value >= target) { 61 + return value; 62 + } 63 + } 64 + return null; 65 + } 66 + 67 + /** 68 + * Find the previous value in a sorted array that is <= target 69 + * Returns null if no such value exists 70 + * 71 + * @param values - MUST be sorted in ascending order 72 + * @param target - The maximum value to find 73 + */ 74 + export function findPrevious(values: number[], target: number): number | null { 75 + for (let i = values.length - 1; i >= 0; i--) { 76 + if (values[i] <= target) { 77 + return values[i]; 78 + } 79 + } 80 + return null; 81 + } 82 + 83 + /** 84 + * Get the number of days in a month 85 + * 86 + * @param year - The year 87 + * @param month - The month (0-indexed: 0 = January, 11 = December) 88 + * @returns The number of days in the month 89 + */ 90 + export function getDaysInMonth(year: number, month: number): number { 91 + // Create date for first day of next month, then go back one day 92 + return new Date(year, month + 1, 0).getDate(); 93 + }
+225
src/parser.ts
··· 1 + import type { ParsedCron } from "./types.js"; 2 + 3 + const MONTH_NAMES: Record<string, number> = { 4 + jan: 1, 5 + feb: 2, 6 + mar: 3, 7 + apr: 4, 8 + may: 5, 9 + jun: 6, 10 + jul: 7, 11 + aug: 8, 12 + sep: 9, 13 + oct: 10, 14 + nov: 11, 15 + dec: 12, 16 + }; 17 + 18 + const WEEKDAY_NAMES: Record<string, number> = { 19 + sun: 0, 20 + mon: 1, 21 + tue: 2, 22 + wed: 3, 23 + thu: 4, 24 + fri: 5, 25 + sat: 6, 26 + }; 27 + 28 + /** 29 + * Parse a cron expression into structured format 30 + * 31 + * Cron format: minute hour day month weekday 32 + * - minute: 0-59 33 + * - hour: 0-23 34 + * - day: 1-31 35 + * - month: 1-12 (or JAN-DEC) 36 + * - weekday: 0-7 (or SUN-SAT, where 0 and 7 are Sunday) 37 + * 38 + * Note: Months are converted from cron's 1-indexed format (1-12) to 39 + * JavaScript's 0-indexed format (0-11) for internal consistency. 40 + */ 41 + export function parse(expression: string): ParsedCron { 42 + const trimmed = expression.trim(); 43 + 44 + if (!trimmed) { 45 + throw new Error("Cron expression cannot be empty"); 46 + } 47 + 48 + const parts = trimmed.split(/\s+/); 49 + 50 + if (parts.length !== 5) { 51 + throw new Error(`Invalid cron expression: expected 5 fields, got ${parts.length}`); 52 + } 53 + 54 + const [minuteStr, hourStr, dayStr, monthStr, weekdayStr] = parts; 55 + 56 + const weekdays = parseField(weekdayStr, 0, 7, WEEKDAY_NAMES).map((d) => (d === 7 ? 0 : d)); 57 + 58 + const parsed: ParsedCron = { 59 + minute: parseField(minuteStr, 0, 59), 60 + hour: parseField(hourStr, 0, 23), 61 + day: parseField(dayStr, 1, 31), 62 + month: parseField(monthStr, 1, 12, MONTH_NAMES).map((m) => m - 1), // Convert to 0-indexed (0 = Jan, 11 = Dec) 63 + weekday: Array.from(new Set(weekdays)).sort((a, b) => a - b), // Dedupe and sort 64 + }; 65 + 66 + // Validate day/month combinations 67 + validateDayMonthCombinations(parsed); 68 + 69 + return parsed; 70 + } 71 + 72 + /** 73 + * Validate that day/month combinations are possible 74 + * Rejects expressions like "0 0 31 2 *" (Feb 31) or "0 0 30 2 *" (Feb 30) 75 + */ 76 + function validateDayMonthCombinations(parsed: ParsedCron): void { 77 + // If day or month is wildcard, no validation needed 78 + const dayIsWildcard = parsed.day.length === 31; 79 + const monthIsWildcard = parsed.month.length === 12; 80 + 81 + if (dayIsWildcard || monthIsWildcard) { 82 + return; 83 + } 84 + 85 + // Days in each month (0-indexed: 0=Jan, 11=Dec) 86 + // February can have 29 days in leap years 87 + const daysInMonth = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; 88 + 89 + // Check if any specified month can accommodate any specified day 90 + let hasValidCombination = false; 91 + 92 + for (const month of parsed.month) { 93 + const maxDaysInMonth = daysInMonth[month]; 94 + 95 + for (const day of parsed.day) { 96 + if (day <= maxDaysInMonth) { 97 + hasValidCombination = true; 98 + break; 99 + } 100 + } 101 + 102 + if (hasValidCombination) { 103 + break; 104 + } 105 + } 106 + 107 + if (!hasValidCombination) { 108 + throw new Error(`Invalid cron expression: no valid day/month combination exists`); 109 + } 110 + } 111 + 112 + /** 113 + * Parse a single cron field (e.g., star-slash-5, 1-10, 1,3,5) 114 + */ 115 + function parseField( 116 + field: string, 117 + min: number, 118 + max: number, 119 + names?: Record<string, number>, 120 + ): number[] { 121 + const values = new Set<number>(); 122 + 123 + // Handle wildcard 124 + if (field === "*") { 125 + for (let i = min; i <= max; i++) { 126 + values.add(i); 127 + } 128 + return Array.from(values).sort((a, b) => a - b); 129 + } 130 + 131 + // Split by comma for multiple values 132 + const parts = field.split(","); 133 + 134 + for (const part of parts) { 135 + // Handle step values (e.g., star-slash-5 or 10-20/2) 136 + if (part.includes("/")) { 137 + const [range, stepStr] = part.split("/"); 138 + const step = parseInt(stepStr, 10); 139 + 140 + if (isNaN(step) || step <= 0) { 141 + throw new Error(`Invalid step value: ${stepStr}`); 142 + } 143 + 144 + let start = min; 145 + let end = max; 146 + 147 + if (range !== "*") { 148 + if (range.includes("-")) { 149 + const [startStr, endStr] = range.split("-"); 150 + start = parseValue(startStr, names); 151 + end = parseValue(endStr, names); 152 + } else { 153 + start = parseValue(range, names); 154 + } 155 + } 156 + 157 + for (let i = start; i <= end; i += step) { 158 + if (i >= min && i <= max) { 159 + values.add(i); 160 + } 161 + } 162 + } 163 + // Handle ranges (e.g., 1-5) 164 + else if (part.includes("-")) { 165 + const [startStr, endStr] = part.split("-"); 166 + const start = parseValue(startStr, names); 167 + const end = parseValue(endStr, names); 168 + 169 + if (start > end) { 170 + throw new Error(`Invalid range: ${part}`); 171 + } 172 + 173 + for (let i = start; i <= end; i++) { 174 + if (i >= min && i <= max) { 175 + values.add(i); 176 + } 177 + } 178 + } 179 + // Handle single values 180 + else { 181 + const value = parseValue(part, names); 182 + if (value >= min && value <= max) { 183 + values.add(value); 184 + } else { 185 + throw new Error(`Value ${value} out of range [${min}-${max}]`); 186 + } 187 + } 188 + } 189 + 190 + if (values.size === 0) { 191 + throw new Error(`No valid values in field: ${field}`); 192 + } 193 + 194 + return Array.from(values).sort((a, b) => a - b); 195 + } 196 + 197 + /** 198 + * Parse a single value (number or name) 199 + */ 200 + function parseValue(value: string, names?: Record<string, number>): number { 201 + const lower = value.toLowerCase(); 202 + 203 + if (names && lower in names) { 204 + return names[lower]; 205 + } 206 + 207 + const num = parseInt(value, 10); 208 + if (isNaN(num)) { 209 + throw new Error(`Invalid value: ${value}`); 210 + } 211 + 212 + return num; 213 + } 214 + 215 + /** 216 + * Validate a cron expression 217 + */ 218 + export function isValid(expression: string): boolean { 219 + try { 220 + parse(expression); 221 + return true; 222 + } catch { 223 + return false; 224 + } 225 + }
+246
src/scheduler.ts
··· 1 + import type { ParsedCron, CronOptions } from "./types.js"; 2 + import { parse } from "./parser.js"; 3 + import { matches, findNext, findPrevious, getDaysInMonth } from "./matcher.js"; 4 + import { convertToTimezone, convertFromTimezone } from "./timezone.js"; 5 + 6 + const MAX_ITERATIONS = 1000; 7 + const ONE_MINUTE_MS = 60_000; 8 + 9 + type Direction = "next" | "prev"; 10 + 11 + /** Direction-specific operations for unified forward/backward traversal */ 12 + const DIR = { 13 + next: { 14 + find: findNext, 15 + minute: (p: ParsedCron) => p.minute[0], 16 + hour: (p: ParsedCron) => p.hour[0], 17 + offset: 1, 18 + }, 19 + prev: { 20 + find: findPrevious, 21 + minute: (p: ParsedCron) => p.minute.at(-1)!, 22 + hour: (p: ParsedCron) => p.hour.at(-1)!, 23 + offset: -1, 24 + }, 25 + } as const; 26 + 27 + /** Get the next execution time for a cron expression */ 28 + export function nextRun(expression: string, options?: CronOptions): Date { 29 + const parsed = parse(expression); 30 + const from = options?.from || new Date(); 31 + const tz = options?.timezone; 32 + 33 + const start = tz ? convertToTimezone(from, tz) : new Date(from); 34 + start.setUTCSeconds(0, 0); 35 + start.setUTCMinutes(start.getUTCMinutes() + 1); 36 + 37 + const result = findMatch(parsed, start, "next", tz); 38 + if (!result) throw new Error("No matching time found within reasonable search window"); 39 + return result; 40 + } 41 + 42 + /** Get the previous execution time for a cron expression */ 43 + export function previousRun(expression: string, options?: CronOptions): Date { 44 + const parsed = parse(expression); 45 + const from = options?.from || new Date(); 46 + const tz = options?.timezone; 47 + 48 + const start = tz ? convertToTimezone(from, tz) : new Date(from); 49 + start.setUTCSeconds(0, 0); 50 + start.setUTCMinutes(start.getUTCMinutes() - 1); 51 + 52 + const result = findMatch(parsed, start, "prev", tz); 53 + if (!result) throw new Error("No matching time found within reasonable search window"); 54 + return result; 55 + } 56 + 57 + /** Get next N execution times */ 58 + export function nextRuns(expression: string, count: number, options?: CronOptions): Date[] { 59 + if (count <= 0) return []; 60 + 61 + const results: Date[] = []; 62 + let current = options?.from || new Date(); 63 + 64 + for (let i = 0; i < count; i++) { 65 + const next = nextRun(expression, { ...options, from: current }); 66 + results.push(next); 67 + current = new Date(next.getTime() + ONE_MINUTE_MS); 68 + } 69 + return results; 70 + } 71 + 72 + /** Check if a date matches the cron expression */ 73 + export function isMatch( 74 + expression: string, 75 + date: Date, 76 + options?: Pick<CronOptions, "timezone">, 77 + ): boolean { 78 + const parsed = parse(expression); 79 + const checkDate = options?.timezone ? convertToTimezone(date, options.timezone) : new Date(date); 80 + return matches(parsed, checkDate); 81 + } 82 + 83 + /** Find matching time using smart field-increment algorithm */ 84 + function findMatch(parsed: ParsedCron, start: Date, dir: Direction, tz?: string): Date | null { 85 + const current = new Date(start); 86 + 87 + for (let i = 0; i < MAX_ITERATIONS; i++) { 88 + if (matches(parsed, current)) { 89 + return tz ? convertFromTimezone(current, tz) : current; 90 + } 91 + advanceDate(parsed, current, dir); 92 + } 93 + return null; 94 + } 95 + 96 + /** 97 + * Advance date to next/prev candidate time by mutating the date in place. 98 + * 99 + * Algorithm: 100 + * 1. Check fields from LARGEST (month) to SMALLEST (minute) 101 + * 2. When a field doesn't match, jump to the next valid value for that field 102 + * 3. Reset all smaller fields to their boundary (first value for 'next', last for 'prev') 103 + * 104 + * Example (direction='next', cron='0 9 * * *' meaning 9:00 AM daily): 105 + * Current: March 15, 10:30 AM 106 + * - Month (March)? ✓ matches 107 + * - Day (15)? ✓ matches 108 + * - Hour (10)? ✗ not in [9] → no next hour today → cascade to next day 109 + * - Result: March 16, 9:00 AM 110 + * 111 + * @param parsed - The parsed cron expression 112 + * @param date - The date to mutate (modified in place) 113 + * @param dir - Direction to advance ('next' or 'prev') 114 + */ 115 + function advanceDate(parsed: ParsedCron, date: Date, dir: Direction): void { 116 + const d = DIR[dir]; 117 + const minute = date.getUTCMinutes(); 118 + const hour = date.getUTCHours(); 119 + const day = date.getUTCDate(); 120 + const month = date.getUTCMonth(); 121 + const year = date.getUTCFullYear(); 122 + const daysInMonth = getDaysInMonth(year, month); 123 + 124 + // Month mismatch 125 + if (!parsed.month.includes(month)) { 126 + moveToMonth(parsed, date, dir, month, year); 127 + return; 128 + } 129 + 130 + // Day mismatch 131 + if (!parsed.day.includes(day) || day > daysInMonth) { 132 + moveToDay(parsed, date, dir, day, month, year, daysInMonth); 133 + return; 134 + } 135 + 136 + // Hour mismatch 137 + if (!parsed.hour.includes(hour)) { 138 + const targetHour = d.find(parsed.hour, hour + d.offset); 139 + if (targetHour !== null) { 140 + // Found valid hour in same day → reset minute to boundary 141 + date.setUTCHours(targetHour); 142 + date.setUTCMinutes(d.minute(parsed)); 143 + } else { 144 + // No valid hour left today → move to next/prev day 145 + moveToDay(parsed, date, dir, day, month, year, daysInMonth); 146 + } 147 + return; 148 + } 149 + 150 + // Minute mismatch 151 + if (!parsed.minute.includes(minute)) { 152 + const targetMinute = d.find(parsed.minute, minute + d.offset); 153 + if (targetMinute !== null) { 154 + // Found valid minute in same hour 155 + date.setUTCMinutes(targetMinute); 156 + } else { 157 + // No valid minute left → try next hour 158 + const targetHour = d.find(parsed.hour, hour + d.offset); 159 + if (targetHour !== null) { 160 + date.setUTCHours(targetHour); 161 + date.setUTCMinutes(d.minute(parsed)); 162 + } else { 163 + // No valid hour left → move to next/prev day 164 + moveToDay(parsed, date, dir, day, month, year, daysInMonth); 165 + } 166 + } 167 + return; 168 + } 169 + 170 + // Weekday mismatch: all fields match but wrong day-of-week. 171 + // Skip directly to next/prev day since no hour/minute on this day can match. 172 + moveToDay(parsed, date, dir, day, month, year, daysInMonth); 173 + } 174 + 175 + function moveToMonth( 176 + parsed: ParsedCron, 177 + date: Date, 178 + dir: Direction, 179 + currentMonth: number, 180 + currentYear: number, 181 + ): void { 182 + const d = DIR[dir]; 183 + const targetMonth = d.find(parsed.month, currentMonth + d.offset); 184 + 185 + if (targetMonth !== null) { 186 + resetToMonthBoundary(parsed, date, currentYear, targetMonth, dir); 187 + } else { 188 + const boundaryMonth = dir === "next" ? parsed.month[0] : parsed.month.at(-1)!; 189 + resetToMonthBoundary(parsed, date, currentYear + d.offset, boundaryMonth, dir); 190 + } 191 + } 192 + 193 + function moveToDay( 194 + parsed: ParsedCron, 195 + date: Date, 196 + dir: Direction, 197 + currentDay: number, 198 + currentMonth: number, 199 + currentYear: number, 200 + daysInMonth: number, 201 + ): void { 202 + const d = DIR[dir]; 203 + const targetDay = d.find(parsed.day, currentDay + d.offset); 204 + const dayIsValid = 205 + dir === "next" ? targetDay !== null && targetDay <= daysInMonth : targetDay !== null; 206 + 207 + if (dayIsValid) { 208 + date.setUTCDate(targetDay!); 209 + date.setUTCHours(d.hour(parsed)); 210 + date.setUTCMinutes(d.minute(parsed)); 211 + } else { 212 + moveToMonth(parsed, date, dir, currentMonth, currentYear); 213 + } 214 + } 215 + 216 + function resetToMonthBoundary( 217 + parsed: ParsedCron, 218 + date: Date, 219 + year: number, 220 + month: number, 221 + dir: Direction, 222 + ): void { 223 + const d = DIR[dir]; 224 + date.setUTCFullYear(year); 225 + date.setUTCDate(1); 226 + date.setUTCMonth(month); 227 + 228 + const daysInMonth = getDaysInMonth(year, month); 229 + 230 + if (dir === "next") { 231 + const validDay = findNext(parsed.day, 1); 232 + date.setUTCDate(validDay !== null && validDay <= daysInMonth ? validDay : parsed.day[0]); 233 + } else { 234 + const prevDay = findPrevious(parsed.day, daysInMonth); 235 + if (prevDay !== null) { 236 + date.setUTCDate(prevDay); 237 + } else { 238 + // No valid day in this month, move to previous month 239 + moveToMonth(parsed, date, dir, month, year); 240 + return; 241 + } 242 + } 243 + 244 + date.setUTCHours(d.hour(parsed)); 245 + date.setUTCMinutes(d.minute(parsed)); 246 + }
+135
src/timezone.ts
··· 1 + /** Convert a UTC date to wall-clock time in the target timezone */ 2 + export function convertToTimezone(date: Date, timezone: string): Date { 3 + // Format the date in the target timezone 4 + const str = date.toLocaleString("en-US", { 5 + timeZone: timezone, 6 + year: "numeric", 7 + month: "2-digit", 8 + day: "2-digit", 9 + hour: "2-digit", 10 + minute: "2-digit", 11 + second: "2-digit", 12 + hour12: false, 13 + }); 14 + 15 + // Parse formatted string: "MM/DD/YYYY, HH:mm:ss" 16 + const [datePart, timePart] = str.split(", "); 17 + const [month, day, year] = datePart.split("/").map(Number); 18 + let [hour, minute, second] = timePart.split(":").map(Number); 19 + 20 + if (hour === 24) hour = 0; // Normalize "24:00:00" to "00:00:00" 21 + 22 + return new Date(Date.UTC(year, month - 1, day, hour, minute, second)); 23 + } 24 + 25 + /** 26 + * Convert a timezone-local date back to UTC (inverse of convertToTimezone). 27 + * 28 + * Note: During DST fall-back, multiple UTC times map to the same wall-clock time. 29 + * The result is implementation-defined. Avoid scheduling during DST transition hours 30 + * for predictable behavior. 31 + */ 32 + export function convertFromTimezone(date: Date, timezone: string): Date { 33 + const targetYear = date.getUTCFullYear(); 34 + const targetMonth = date.getUTCMonth(); 35 + const targetDay = date.getUTCDate(); 36 + const targetHour = date.getUTCHours(); 37 + const targetMinute = date.getUTCMinutes(); 38 + const targetSecond = date.getUTCSeconds(); 39 + 40 + // Target time as a comparable number (for checking if we found it) 41 + const targetTime = Date.UTC( 42 + targetYear, 43 + targetMonth, 44 + targetDay, 45 + targetHour, 46 + targetMinute, 47 + targetSecond, 48 + ); 49 + 50 + // Start with a guess: interpret the wall-clock time as UTC 51 + let guess = targetTime; 52 + let bestGuess = guess; 53 + let bestDiff = Infinity; 54 + 55 + // Iteratively refine the guess (usually converges in 1-2 iterations) 56 + for (let i = 0; i < 3; i++) { 57 + const testDate = new Date(guess); 58 + const testStr = testDate.toLocaleString("en-US", { 59 + timeZone: timezone, 60 + year: "numeric", 61 + month: "2-digit", 62 + day: "2-digit", 63 + hour: "2-digit", 64 + minute: "2-digit", 65 + second: "2-digit", 66 + hour12: false, 67 + }); 68 + 69 + // Parse what wall-clock time this guess produces 70 + const [testDatePart, testTimePart] = testStr.split(", "); 71 + const [testMonth, testDay, testYear] = testDatePart.split("/").map(Number); 72 + let [testHour, testMinute, testSecond] = testTimePart.split(":").map(Number); 73 + 74 + if (testHour === 24) testHour = 0; // Normalize "24:00:00" to "00:00:00" 75 + 76 + const gotTime = Date.UTC(testYear, testMonth - 1, testDay, testHour, testMinute, testSecond); 77 + 78 + // Track the best guess (closest to target, but prefer later times if equal distance) 79 + const diff = Math.abs(targetTime - gotTime); 80 + if (diff < bestDiff || (diff === bestDiff && guess > bestGuess)) { 81 + bestDiff = diff; 82 + bestGuess = guess; 83 + } 84 + 85 + // If we got what we wanted, we're done! 86 + // Note: During DST fall-back, two UTC times map to the same wall-clock time. 87 + // This returns whichever solution the iteration converges to first (implementation-defined). 88 + if (gotTime === targetTime) { 89 + return new Date(guess); 90 + } 91 + 92 + // Otherwise, adjust the guess by the difference 93 + const adjustment = targetTime - gotTime; 94 + guess += adjustment; 95 + } 96 + 97 + // If we didn't find an exact match after 3 iterations, we're likely in a DST gap 98 + // (e.g., 2:30 AM during spring forward doesn't exist) 99 + // Try one more time: check if adding 1 hour to the target gets us closer 100 + const oneHourLater = targetTime + 60 * 60 * 1000; 101 + let guessLater = oneHourLater; 102 + 103 + for (let i = 0; i < 2; i++) { 104 + const testDate = new Date(guessLater); 105 + const testStr = testDate.toLocaleString("en-US", { 106 + timeZone: timezone, 107 + year: "numeric", 108 + month: "2-digit", 109 + day: "2-digit", 110 + hour: "2-digit", 111 + minute: "2-digit", 112 + second: "2-digit", 113 + hour12: false, 114 + }); 115 + 116 + const [testDatePart, testTimePart] = testStr.split(", "); 117 + const [testMonth, testDay, testYear] = testDatePart.split("/").map(Number); 118 + let [testHour, testMinute, testSecond] = testTimePart.split(":").map(Number); 119 + 120 + if (testHour === 24) testHour = 0; // Normalize "24:00:00" to "00:00:00" 121 + 122 + const gotTime = Date.UTC(testYear, testMonth - 1, testDay, testHour, testMinute, testSecond); 123 + 124 + if (gotTime === oneHourLater) { 125 + // Target time was in a DST gap, return the time after the gap 126 + return new Date(guessLater); 127 + } 128 + 129 + const adjustment = oneHourLater - gotTime; 130 + guessLater += adjustment; 131 + } 132 + 133 + // Return the best guess we found 134 + return new Date(bestGuess); 135 + }
+24
src/types.ts
··· 1 + /** 2 + * Options for cron execution time calculations 3 + */ 4 + export interface CronOptions { 5 + /** IANA timezone string (e.g., 'America/New_York', 'Europe/London') */ 6 + timezone?: string; 7 + /** Reference date to calculate from (defaults to now) */ 8 + from?: Date; 9 + } 10 + 11 + /** 12 + * Parsed cron expression with valid values for each field 13 + * 14 + * Note: Internally, months are stored as 0-indexed (0 = January, 11 = December) 15 + * to match JavaScript's Date object convention. The parser automatically converts 16 + * from cron's 1-indexed format (1-12) to 0-indexed (0-11). 17 + */ 18 + export interface ParsedCron { 19 + minute: number[]; // 0-59 20 + hour: number[]; // 0-23 21 + day: number[]; // 1-31 22 + month: number[]; // 0-11 (0 = January, 11 = December) 23 + weekday: number[]; // 0-6 (0 = Sunday, 6 = Saturday) 24 + }
+255
test/matcher.test.ts
··· 1 + import { describe, it, expect } from "vitest"; 2 + import { matches, findNext, findPrevious, getDaysInMonth } from "../src/matcher.js"; 3 + import { parse } from "../src/parser.js"; 4 + 5 + describe("matcher", () => { 6 + describe("matches", () => { 7 + it("should match exact time", () => { 8 + const parsed = parse("30 14 * * *"); 9 + const date = new Date("2026-03-15T14:30:00Z"); 10 + 11 + expect(matches(parsed, date)).toBe(true); 12 + }); 13 + 14 + it("should not match different minute", () => { 15 + const parsed = parse("30 14 * * *"); 16 + const date = new Date("2026-03-15T14:31:00Z"); 17 + 18 + expect(matches(parsed, date)).toBe(false); 19 + }); 20 + 21 + it("should not match different hour", () => { 22 + const parsed = parse("30 14 * * *"); 23 + const date = new Date("2026-03-15T15:30:00Z"); 24 + 25 + expect(matches(parsed, date)).toBe(false); 26 + }); 27 + 28 + it("should match wildcard minute", () => { 29 + const parsed = parse("* 14 * * *"); 30 + const date1 = new Date("2026-03-15T14:00:00Z"); 31 + const date2 = new Date("2026-03-15T14:30:00Z"); 32 + const date3 = new Date("2026-03-15T14:59:00Z"); 33 + 34 + expect(matches(parsed, date1)).toBe(true); 35 + expect(matches(parsed, date2)).toBe(true); 36 + expect(matches(parsed, date3)).toBe(true); 37 + }); 38 + 39 + it("should match step values", () => { 40 + const parsed = parse("*/15 * * * *"); 41 + const date1 = new Date("2026-03-15T14:00:00Z"); 42 + const date2 = new Date("2026-03-15T14:15:00Z"); 43 + const date3 = new Date("2026-03-15T14:30:00Z"); 44 + const date4 = new Date("2026-03-15T14:45:00Z"); 45 + const date5 = new Date("2026-03-15T14:10:00Z"); 46 + 47 + expect(matches(parsed, date1)).toBe(true); 48 + expect(matches(parsed, date2)).toBe(true); 49 + expect(matches(parsed, date3)).toBe(true); 50 + expect(matches(parsed, date4)).toBe(true); 51 + expect(matches(parsed, date5)).toBe(false); 52 + }); 53 + 54 + it("should match specific day of month", () => { 55 + const parsed = parse("0 9 15 * *"); 56 + const date1 = new Date("2026-03-15T09:00:00Z"); 57 + const date2 = new Date("2026-03-16T09:00:00Z"); 58 + 59 + expect(matches(parsed, date1)).toBe(true); 60 + expect(matches(parsed, date2)).toBe(false); 61 + }); 62 + 63 + it("should match specific month", () => { 64 + const parsed = parse("0 9 * 3 *"); 65 + const date1 = new Date("2026-03-15T09:00:00Z"); 66 + const date2 = new Date("2026-04-15T09:00:00Z"); 67 + 68 + expect(matches(parsed, date1)).toBe(true); 69 + expect(matches(parsed, date2)).toBe(false); 70 + }); 71 + 72 + it("should match specific weekday", () => { 73 + const parsed = parse("0 9 * * 1"); // Monday 74 + const monday = new Date("2026-03-16T09:00:00Z"); // Monday 75 + const tuesday = new Date("2026-03-17T09:00:00Z"); // Tuesday 76 + 77 + expect(matches(parsed, monday)).toBe(true); 78 + expect(matches(parsed, tuesday)).toBe(false); 79 + }); 80 + 81 + it("should use OR logic when both day and weekday are specified", () => { 82 + // Run on 15th OR on Mondays 83 + const parsed = parse("0 9 15 * 1"); 84 + const june15Monday = new Date("2026-06-15T09:00:00Z"); // 15th AND Monday 85 + const march16Monday = new Date("2026-03-16T09:00:00Z"); // 16th but Monday 86 + const march15Sunday = new Date("2026-03-15T09:00:00Z"); // 15th but Sunday 87 + const march17Tuesday = new Date("2026-03-17T09:00:00Z"); // 17th and Tuesday 88 + 89 + expect(matches(parsed, june15Monday)).toBe(true); // Matches both day=15 AND weekday=Monday 90 + expect(matches(parsed, march16Monday)).toBe(true); // Matches weekday=Monday only 91 + expect(matches(parsed, march15Sunday)).toBe(true); // Matches day=15 only 92 + expect(matches(parsed, march17Tuesday)).toBe(false); // Matches neither 93 + }); 94 + 95 + it("should match range of values", () => { 96 + const parsed = parse("0 9-17 * * *"); // 9 AM to 5 PM 97 + const date1 = new Date("2026-03-15T09:00:00Z"); 98 + const date2 = new Date("2026-03-15T12:00:00Z"); 99 + const date3 = new Date("2026-03-15T17:00:00Z"); 100 + const date4 = new Date("2026-03-15T08:00:00Z"); 101 + const date5 = new Date("2026-03-15T18:00:00Z"); 102 + 103 + expect(matches(parsed, date1)).toBe(true); 104 + expect(matches(parsed, date2)).toBe(true); 105 + expect(matches(parsed, date3)).toBe(true); 106 + expect(matches(parsed, date4)).toBe(false); 107 + expect(matches(parsed, date5)).toBe(false); 108 + }); 109 + 110 + it("should match list of values", () => { 111 + const parsed = parse("0 9,12,15 * * *"); 112 + const date1 = new Date("2026-03-15T09:00:00Z"); 113 + const date2 = new Date("2026-03-15T12:00:00Z"); 114 + const date3 = new Date("2026-03-15T15:00:00Z"); 115 + const date4 = new Date("2026-03-15T10:00:00Z"); 116 + 117 + expect(matches(parsed, date1)).toBe(true); 118 + expect(matches(parsed, date2)).toBe(true); 119 + expect(matches(parsed, date3)).toBe(true); 120 + expect(matches(parsed, date4)).toBe(false); 121 + }); 122 + }); 123 + 124 + describe("findNext", () => { 125 + it("should find next value equal to target", () => { 126 + const values = [0, 15, 30, 45]; 127 + expect(findNext(values, 15)).toBe(15); 128 + }); 129 + 130 + it("should find next value greater than target", () => { 131 + const values = [0, 15, 30, 45]; 132 + expect(findNext(values, 16)).toBe(30); 133 + }); 134 + 135 + it("should return first value if target is before all", () => { 136 + const values = [0, 15, 30, 45]; 137 + expect(findNext(values, -1)).toBe(0); 138 + }); 139 + 140 + it("should return null if target is after all values", () => { 141 + const values = [0, 15, 30, 45]; 142 + expect(findNext(values, 50)).toBeNull(); 143 + }); 144 + 145 + it("should work with single value", () => { 146 + const values = [30]; 147 + expect(findNext(values, 20)).toBe(30); 148 + expect(findNext(values, 30)).toBe(30); 149 + expect(findNext(values, 40)).toBeNull(); 150 + }); 151 + 152 + it("should work with unsorted values", () => { 153 + const values = [30, 0, 45, 15]; 154 + expect(findNext(values, 10)).toBe(30); 155 + }); 156 + }); 157 + 158 + describe("findPrevious", () => { 159 + it("should find previous value equal to target", () => { 160 + const values = [0, 15, 30, 45]; 161 + expect(findPrevious(values, 30)).toBe(30); 162 + }); 163 + 164 + it("should find previous value less than target", () => { 165 + const values = [0, 15, 30, 45]; 166 + expect(findPrevious(values, 35)).toBe(30); 167 + }); 168 + 169 + it("should return last value if target is after all", () => { 170 + const values = [0, 15, 30, 45]; 171 + expect(findPrevious(values, 50)).toBe(45); 172 + }); 173 + 174 + it("should return null if target is before all values", () => { 175 + const values = [0, 15, 30, 45]; 176 + expect(findPrevious(values, -1)).toBeNull(); 177 + }); 178 + 179 + it("should work with single value", () => { 180 + const values = [30]; 181 + expect(findPrevious(values, 40)).toBe(30); 182 + expect(findPrevious(values, 30)).toBe(30); 183 + expect(findPrevious(values, 20)).toBeNull(); 184 + }); 185 + 186 + it("should work with unsorted values", () => { 187 + const values = [30, 0, 45, 15]; 188 + expect(findPrevious(values, 20)).toBe(15); 189 + }); 190 + }); 191 + 192 + describe("getDaysInMonth", () => { 193 + it("should return 31 for January", () => { 194 + expect(getDaysInMonth(2026, 0)).toBe(31); 195 + }); 196 + 197 + it("should return 28 for February in non-leap year", () => { 198 + expect(getDaysInMonth(2026, 1)).toBe(28); 199 + expect(getDaysInMonth(2027, 1)).toBe(28); 200 + }); 201 + 202 + it("should return 29 for February in leap year", () => { 203 + expect(getDaysInMonth(2028, 1)).toBe(29); 204 + expect(getDaysInMonth(2024, 1)).toBe(29); 205 + }); 206 + 207 + it("should return 31 for March", () => { 208 + expect(getDaysInMonth(2026, 2)).toBe(31); 209 + }); 210 + 211 + it("should return 30 for April", () => { 212 + expect(getDaysInMonth(2026, 3)).toBe(30); 213 + }); 214 + 215 + it("should return 31 for May", () => { 216 + expect(getDaysInMonth(2026, 4)).toBe(31); 217 + }); 218 + 219 + it("should return 30 for June", () => { 220 + expect(getDaysInMonth(2026, 5)).toBe(30); 221 + }); 222 + 223 + it("should return 31 for July", () => { 224 + expect(getDaysInMonth(2026, 6)).toBe(31); 225 + }); 226 + 227 + it("should return 31 for August", () => { 228 + expect(getDaysInMonth(2026, 7)).toBe(31); 229 + }); 230 + 231 + it("should return 30 for September", () => { 232 + expect(getDaysInMonth(2026, 8)).toBe(30); 233 + }); 234 + 235 + it("should return 31 for October", () => { 236 + expect(getDaysInMonth(2026, 9)).toBe(31); 237 + }); 238 + 239 + it("should return 30 for November", () => { 240 + expect(getDaysInMonth(2026, 10)).toBe(30); 241 + }); 242 + 243 + it("should return 31 for December", () => { 244 + expect(getDaysInMonth(2026, 11)).toBe(31); 245 + }); 246 + 247 + it("should handle leap year 2000 (divisible by 400)", () => { 248 + expect(getDaysInMonth(2000, 1)).toBe(29); 249 + }); 250 + 251 + it("should handle non-leap year 1900 (divisible by 100 but not 400)", () => { 252 + expect(getDaysInMonth(1900, 1)).toBe(28); 253 + }); 254 + }); 255 + });
+126
test/parser.test.ts
··· 1 + import { describe, it, expect } from "vitest"; 2 + import { parse, isValid } from "../src/parser.js"; 3 + 4 + describe("parser", () => { 5 + describe("parse", () => { 6 + it("should parse simple wildcard expression", () => { 7 + const result = parse("* * * * *"); 8 + expect(result.minute).toHaveLength(60); 9 + expect(result.hour).toHaveLength(24); 10 + expect(result.day).toHaveLength(31); 11 + expect(result.month).toHaveLength(12); 12 + expect(result.weekday).toHaveLength(7); 13 + }); 14 + 15 + it("should parse specific values", () => { 16 + const result = parse("30 9 15 6 1"); 17 + expect(result.minute).toEqual([30]); 18 + expect(result.hour).toEqual([9]); 19 + expect(result.day).toEqual([15]); 20 + expect(result.month).toEqual([5]); // June = 5 (0-indexed) 21 + expect(result.weekday).toEqual([1]); 22 + }); 23 + 24 + it("should parse ranges", () => { 25 + const result = parse("0-30 9-17 * * 1-5"); 26 + expect(result.minute).toHaveLength(31); 27 + expect(result.minute[0]).toBe(0); 28 + expect(result.minute[30]).toBe(30); 29 + expect(result.hour).toEqual([9, 10, 11, 12, 13, 14, 15, 16, 17]); 30 + expect(result.weekday).toEqual([1, 2, 3, 4, 5]); 31 + }); 32 + 33 + it("should parse step values", () => { 34 + const result = parse("*/15 */6 * * *"); 35 + expect(result.minute).toEqual([0, 15, 30, 45]); 36 + expect(result.hour).toEqual([0, 6, 12, 18]); 37 + }); 38 + 39 + it("should parse step values with ranges", () => { 40 + const result = parse("10-40/10 * * * *"); 41 + expect(result.minute).toEqual([10, 20, 30, 40]); 42 + }); 43 + 44 + it("should parse comma-separated values", () => { 45 + const result = parse("0,15,30,45 9,12,15 * * *"); 46 + expect(result.minute).toEqual([0, 15, 30, 45]); 47 + expect(result.hour).toEqual([9, 12, 15]); 48 + }); 49 + 50 + it("should parse month names", () => { 51 + const result = parse("0 0 1 jan,jun,dec *"); 52 + expect(result.month).toEqual([0, 5, 11]); // Jan=0, Jun=5, Dec=11 (0-indexed) 53 + }); 54 + 55 + it("should parse weekday names", () => { 56 + const result = parse("0 9 * * mon-fri"); 57 + expect(result.weekday).toEqual([1, 2, 3, 4, 5]); 58 + }); 59 + 60 + it("should normalize weekday 7 to 0", () => { 61 + const result = parse("0 0 * * 7"); 62 + expect(result.weekday).toEqual([0]); 63 + }); 64 + 65 + it("should handle mixed case names", () => { 66 + const result = parse("0 0 * JAN,FEB *"); 67 + expect(result.month).toEqual([0, 1]); // Jan=0, Feb=1 (0-indexed) 68 + }); 69 + 70 + it("should throw on empty expression", () => { 71 + expect(() => parse("")).toThrow("cannot be empty"); 72 + }); 73 + 74 + it("should throw on wrong number of fields", () => { 75 + expect(() => parse("* * *")).toThrow("expected 5 fields"); 76 + }); 77 + 78 + it("should throw on invalid step value", () => { 79 + expect(() => parse("*/0 * * * *")).toThrow("Invalid step"); 80 + }); 81 + 82 + it("should throw on invalid range", () => { 83 + expect(() => parse("50-10 * * * *")).toThrow("Invalid range"); 84 + }); 85 + 86 + it("should throw on out of range value", () => { 87 + expect(() => parse("60 * * * *")).toThrow("out of range"); 88 + }); 89 + 90 + it("should throw on impossible day/month combination (Feb 31)", () => { 91 + expect(() => parse("0 0 31 2 *")).toThrow("no valid day/month combination"); 92 + }); 93 + 94 + it("should throw on impossible day/month combination (Feb 30)", () => { 95 + expect(() => parse("0 0 30 2 *")).toThrow("no valid day/month combination"); 96 + }); 97 + 98 + it("should allow Feb 29 (exists in leap years)", () => { 99 + expect(() => parse("0 0 29 2 *")).not.toThrow(); 100 + }); 101 + 102 + it("should allow day 31 with wildcard month", () => { 103 + expect(() => parse("0 0 31 * *")).not.toThrow(); 104 + }); 105 + 106 + it("should allow wildcard day with specific month", () => { 107 + expect(() => parse("0 0 * 2 *")).not.toThrow(); 108 + }); 109 + }); 110 + 111 + describe("isValid", () => { 112 + it("should return true for valid expressions", () => { 113 + expect(isValid("* * * * *")).toBe(true); 114 + expect(isValid("0 9 * * *")).toBe(true); 115 + expect(isValid("*/15 * * * *")).toBe(true); 116 + expect(isValid("0 9 * * mon-fri")).toBe(true); 117 + }); 118 + 119 + it("should return false for invalid expressions", () => { 120 + expect(isValid("")).toBe(false); 121 + expect(isValid("invalid")).toBe(false); 122 + expect(isValid("* * *")).toBe(false); 123 + expect(isValid("60 * * * *")).toBe(false); 124 + }); 125 + }); 126 + });
+955
test/scheduler.test.ts
··· 1 + import { describe, it, expect, beforeEach, afterEach } from "vitest"; 2 + import { nextRun, previousRun, nextRuns, isMatch } from "../src/scheduler.js"; 3 + import { getDaysInMonth } from "../src/matcher.js"; 4 + 5 + describe("scheduler", () => { 6 + describe("Basic functionality", () => { 7 + describe("nextRun", () => { 8 + it("should find next run for simple expression", () => { 9 + const from = new Date("2026-03-15T14:30:00Z"); 10 + const next = nextRun("0 15 * * *", { from }); 11 + 12 + expect(next.getUTCHours()).toBe(15); 13 + expect(next.getUTCMinutes()).toBe(0); 14 + expect(next.getUTCDate()).toBe(15); 15 + }); 16 + 17 + it("should roll over to next day", () => { 18 + const from = new Date("2026-03-15T23:30:00Z"); 19 + const next = nextRun("0 9 * * *", { from }); 20 + 21 + expect(next.getUTCDate()).toBe(16); 22 + expect(next.getUTCHours()).toBe(9); 23 + expect(next.getUTCMinutes()).toBe(0); 24 + }); 25 + 26 + it("should handle every 15 minutes", () => { 27 + const from = new Date("2026-03-15T14:07:00Z"); 28 + const next = nextRun("*/15 * * * *", { from }); 29 + 30 + expect(next.getUTCMinutes()).toBe(15); 31 + }); 32 + 33 + it("should handle specific weekdays", () => { 34 + // Mar 16, 2026 is Monday 35 + const from = new Date("2026-03-16T09:00:00Z"); 36 + const next = nextRun("0 9 * * 1", { from }); // Every Monday at 9 AM 37 + 38 + // Should be next Monday (Mar 23) 39 + expect(next.getUTCDate()).toBe(23); 40 + expect(next.getUTCDay()).toBe(1); // Monday 41 + }); 42 + 43 + it("should handle month boundaries", () => { 44 + const from = new Date("2026-03-31T23:00:00Z"); 45 + const next = nextRun("0 9 1 * *", { from }); // 1st of month at 9 AM 46 + 47 + expect(next.getUTCMonth()).toBe(3); // April 48 + expect(next.getUTCDate()).toBe(1); 49 + }); 50 + 51 + it("should handle year boundaries", () => { 52 + const from = new Date("2026-12-31T23:00:00Z"); 53 + const next = nextRun("0 0 1 1 *", { from }); // Jan 1 at midnight 54 + 55 + expect(next.getUTCFullYear()).toBe(2027); 56 + expect(next.getUTCMonth()).toBe(0); 57 + expect(next.getUTCDate()).toBe(1); 58 + }); 59 + 60 + it("should work with timezone", () => { 61 + const from = new Date("2026-03-15T14:00:00Z"); // 2 PM UTC = 10 AM EDT (DST started Mar 8, 2026) 62 + const next = nextRun("0 10 * * *", { from, timezone: "America/New_York" }); 63 + 64 + // Next 10 AM EDT should be same day (after DST, EDT is UTC-4) 65 + expect(next.getUTCHours()).toBe(14); // 10 AM EDT = 2 PM UTC 66 + }); 67 + }); 68 + 69 + describe("previousRun", () => { 70 + it("should find previous run", () => { 71 + const from = new Date("2026-03-15T14:30:00Z"); 72 + const prev = previousRun("0 9 * * *", { from }); 73 + 74 + expect(prev.getUTCHours()).toBe(9); 75 + expect(prev.getUTCMinutes()).toBe(0); 76 + expect(prev.getUTCDate()).toBe(15); 77 + }); 78 + 79 + it("should roll back to previous day", () => { 80 + const from = new Date("2026-03-15T08:00:00Z"); 81 + const prev = previousRun("0 9 * * *", { from }); 82 + 83 + expect(prev.getUTCDate()).toBe(14); 84 + expect(prev.getUTCHours()).toBe(9); 85 + }); 86 + }); 87 + 88 + describe("nextRuns", () => { 89 + it("should return multiple next runs", () => { 90 + const from = new Date("2026-03-15T14:00:00Z"); 91 + const runs = nextRuns("0 9 * * *", 3, { from }); 92 + 93 + expect(runs).toHaveLength(3); 94 + expect(runs[0].getUTCDate()).toBe(16); 95 + expect(runs[1].getUTCDate()).toBe(17); 96 + expect(runs[2].getUTCDate()).toBe(18); 97 + }); 98 + 99 + it("should handle empty count", () => { 100 + const runs = nextRuns("* * * * *", 0); 101 + expect(runs).toHaveLength(0); 102 + }); 103 + }); 104 + 105 + describe("isMatch", () => { 106 + it("should match exact time", () => { 107 + const date = new Date("2026-03-16T09:00:00Z"); 108 + expect(isMatch("0 9 * * *", date)).toBe(true); 109 + }); 110 + 111 + it("should not match different time", () => { 112 + const date = new Date("2026-03-16T09:30:00Z"); 113 + expect(isMatch("0 9 * * *", date)).toBe(false); 114 + }); 115 + 116 + it("should match with step values", () => { 117 + const date = new Date("2026-03-16T09:15:00Z"); 118 + expect(isMatch("*/15 * * * *", date)).toBe(true); 119 + }); 120 + 121 + it("should match weekday", () => { 122 + const date = new Date("2026-03-16T09:00:00Z"); // Monday 123 + expect(isMatch("0 9 * * 1", date)).toBe(true); 124 + expect(isMatch("0 9 * * 2", date)).toBe(false); 125 + }); 126 + }); 127 + }); 128 + 129 + describe("ISO 8601 date support", () => { 130 + describe("parsing ISO 8601 strings as input", () => { 131 + it("should work with UTC dates (Z suffix)", () => { 132 + const from = new Date("2026-03-15T14:30:00Z"); 133 + const next = nextRun("0 15 * * *", { from }); 134 + 135 + expect(next.getUTCHours()).toBe(15); 136 + expect(next.getUTCMinutes()).toBe(0); 137 + }); 138 + 139 + it("should work with timezone offset (+HH:MM)", () => { 140 + // 9:30 AM EST = 2:30 PM UTC 141 + const from = new Date("2026-03-15T09:30:00-05:00"); 142 + const next = nextRun("0 15 * * *", { from }); 143 + 144 + // Should find next 3 PM UTC 145 + expect(next.getUTCHours()).toBe(15); 146 + expect(next.getUTCMinutes()).toBe(0); 147 + expect(next.getUTCDate()).toBe(15); 148 + }); 149 + 150 + it("should work with positive timezone offset", () => { 151 + // 8:30 PM in UTC+6 = 2:30 PM UTC 152 + const from = new Date("2026-03-15T20:30:00+06:00"); 153 + const next = nextRun("0 15 * * *", { from }); 154 + 155 + // Should find next 3 PM UTC (same day since we're at 2:30 PM) 156 + expect(next.getUTCHours()).toBe(15); 157 + expect(next.getUTCDate()).toBe(15); 158 + }); 159 + 160 + it("should work with milliseconds", () => { 161 + const from = new Date("2026-03-15T14:30:00.500Z"); 162 + const next = nextRun("0 15 * * *", { from }); 163 + 164 + expect(next.getUTCHours()).toBe(15); 165 + expect(next.getUTCMinutes()).toBe(0); 166 + expect(next.getUTCSeconds()).toBe(0); 167 + expect(next.getUTCMilliseconds()).toBe(0); 168 + }); 169 + 170 + it("should treat different representations of same moment equally", () => { 171 + // All these represent the same moment: Mar 15, 2026 at 2:30 PM UTC 172 + const utc = new Date("2026-03-15T14:30:00Z"); 173 + const est = new Date("2026-03-15T09:30:00-05:00"); 174 + const plus6 = new Date("2026-03-15T20:30:00+06:00"); 175 + 176 + // All should produce the same next run 177 + const nextUtc = nextRun("0 15 * * *", { from: utc }); 178 + const nextEst = nextRun("0 15 * * *", { from: est }); 179 + const nextPlus6 = nextRun("0 15 * * *", { from: plus6 }); 180 + 181 + expect(nextUtc.getTime()).toBe(nextEst.getTime()); 182 + expect(nextUtc.getTime()).toBe(nextPlus6.getTime()); 183 + }); 184 + }); 185 + 186 + describe("output format", () => { 187 + it("should always return dates that produce UTC ISO strings", () => { 188 + const next = nextRun("0 9 * * *"); 189 + const isoString = next.toISOString(); 190 + 191 + // Should end with Z (UTC) 192 + expect(isoString).toMatch(/Z$/); 193 + 194 + // Should be valid ISO 8601 195 + expect(isoString).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); 196 + }); 197 + 198 + it("should return dates that can be parsed back", () => { 199 + const next = nextRun("0 9 * * *"); 200 + const isoString = next.toISOString(); 201 + 202 + // Should be able to parse it back 203 + const parsed = new Date(isoString); 204 + expect(parsed.getTime()).toBe(next.getTime()); 205 + }); 206 + }); 207 + 208 + describe("timezone-aware calculations with ISO 8601", () => { 209 + it("should handle timezone option with ISO 8601 input", () => { 210 + // Start at 2 PM UTC (10 AM EDT - DST started Mar 8, 2026) 211 + const from = new Date("2026-03-15T14:00:00Z"); 212 + 213 + // Find next 10 AM EDT 214 + const next = nextRun("0 10 * * *", { 215 + from, 216 + timezone: "America/New_York", 217 + }); 218 + 219 + // 10 AM EDT = 2 PM UTC (EDT is UTC-4) 220 + expect(next.getUTCHours()).toBe(14); 221 + expect(next.toISOString()).toMatch(/Z$/); 222 + }); 223 + 224 + it("should work with offset input and timezone option", () => { 225 + // Start at 9 AM EDT (using offset notation - DST started Mar 8, 2026, so EDT is UTC-4) 226 + const from = new Date("2026-03-15T09:00:00-04:00"); 227 + 228 + // Find next 10 AM EDT 229 + const next = nextRun("0 10 * * *", { 230 + from, 231 + timezone: "America/New_York", 232 + }); 233 + 234 + // Should be same day at 10 AM EDT = 2 PM UTC 235 + expect(next.getUTCHours()).toBe(14); 236 + expect(next.getUTCDate()).toBe(15); 237 + }); 238 + }); 239 + 240 + describe("isMatch with ISO 8601", () => { 241 + it("should match with UTC ISO string", () => { 242 + const date = new Date("2026-03-16T09:00:00Z"); 243 + expect(isMatch("0 9 * * *", date)).toBe(true); 244 + }); 245 + 246 + it("should match with offset ISO string", () => { 247 + // 9 AM UTC = 4 AM EST 248 + const date = new Date("2026-03-16T04:00:00-05:00"); 249 + expect(isMatch("0 9 * * *", date)).toBe(true); 250 + }); 251 + 252 + it("should match with timezone option and ISO input", () => { 253 + // 9 AM EDT in UTC notation (DST started Mar 8, 2026, so EDT is UTC-4) 254 + const date = new Date("2026-03-16T13:00:00Z"); 255 + expect(isMatch("0 9 * * *", date, { timezone: "America/New_York" })).toBe(true); 256 + }); 257 + }); 258 + 259 + describe("previousRun with ISO 8601", () => { 260 + it("should work with UTC ISO string", () => { 261 + const from = new Date("2026-03-15T14:30:00Z"); 262 + const prev = previousRun("0 9 * * *", { from }); 263 + 264 + expect(prev.getUTCHours()).toBe(9); 265 + expect(prev.getUTCDate()).toBe(15); 266 + }); 267 + 268 + it("should work with offset ISO string", () => { 269 + // 2:30 PM EST = 7:30 PM UTC 270 + const from = new Date("2026-03-15T14:30:00-05:00"); 271 + const prev = previousRun("0 9 * * *", { from }); 272 + 273 + // Previous 9 AM UTC 274 + expect(prev.getUTCHours()).toBe(9); 275 + expect(prev.getUTCDate()).toBe(15); 276 + }); 277 + }); 278 + 279 + describe("edge cases", () => { 280 + it("should handle dates near DST transitions", () => { 281 + // March 8, 2026 - DST starts in US (2 AM -> 3 AM) 282 + const from = new Date("2026-03-08T06:00:00Z"); // 1 AM EST 283 + const next = nextRun("0 9 * * *", { 284 + from, 285 + timezone: "America/New_York", 286 + }); 287 + 288 + // Should find 9 AM EDT (which is 1 PM UTC after DST - EDT is UTC-4) 289 + expect(next.getUTCHours()).toBe(13); // 9 AM EDT = 1 PM UTC 290 + expect(next.getUTCDate()).toBe(8); 291 + }); 292 + 293 + it("should handle year boundaries with ISO dates", () => { 294 + const from = new Date("2026-12-31T23:00:00Z"); 295 + const next = nextRun("0 0 1 1 *", { from }); 296 + 297 + expect(next.getUTCFullYear()).toBe(2027); 298 + expect(next.getUTCMonth()).toBe(0); 299 + expect(next.getUTCDate()).toBe(1); 300 + expect(next.toISOString()).toMatch(/^2027-01-01T00:00:00\.000Z$/); 301 + }); 302 + 303 + it("should handle leap year with ISO dates", () => { 304 + // Feb 29, 2028 exists (leap year) 305 + const from = new Date("2028-02-28T12:00:00Z"); 306 + const next = nextRun("0 9 29 2 *", { from }); 307 + 308 + expect(next.getUTCFullYear()).toBe(2028); 309 + expect(next.getUTCMonth()).toBe(1); // February 310 + expect(next.getUTCDate()).toBe(29); 311 + }); 312 + 313 + it("should handle various ISO 8601 formats", () => { 314 + const formats = [ 315 + "2026-03-15T14:30:00Z", // Basic UTC 316 + "2026-03-15T14:30:00.000Z", // With milliseconds 317 + "2026-03-15T09:30:00-05:00", // With negative offset 318 + "2026-03-15T20:30:00+06:00", // With positive offset 319 + "2026-03-15T14:30:00.123Z", // With fractional seconds 320 + ]; 321 + 322 + // All represent the same moment (or close to it) 323 + const results = formats.map((format) => { 324 + const from = new Date(format); 325 + return nextRun("0 15 * * *", { from }); 326 + }); 327 + 328 + // All should produce the same or very close results 329 + const firstTime = results[0].getTime(); 330 + results.forEach((result) => { 331 + expect(Math.abs(result.getTime() - firstTime)).toBeLessThan(1000); 332 + }); 333 + }); 334 + }); 335 + 336 + describe("round-trip compatibility", () => { 337 + it("should maintain precision through parse-format-parse cycle", () => { 338 + const original = nextRun("0 9 * * *"); 339 + const isoString = original.toISOString(); 340 + const parsed = new Date(isoString); 341 + 342 + expect(parsed.getTime()).toBe(original.getTime()); 343 + }); 344 + 345 + it("should work with dates from external systems", () => { 346 + // Simulate dates from various sources 347 + const postgresDate = "2026-03-15T14:30:00+00:00"; 348 + const mongoDate = "2026-03-15T14:30:00.000Z"; 349 + const apiDate = "2026-03-15T09:30:00-05:00"; 350 + 351 + const next1 = nextRun("0 15 * * *", { from: new Date(postgresDate) }); 352 + const next2 = nextRun("0 15 * * *", { from: new Date(mongoDate) }); 353 + const next3 = nextRun("0 15 * * *", { from: new Date(apiDate) }); 354 + 355 + // All should produce the same result 356 + expect(next1.getTime()).toBe(next2.getTime()); 357 + expect(next1.getTime()).toBe(next3.getTime()); 358 + }); 359 + }); 360 + }); 361 + 362 + describe("Month boundaries and leap years", () => { 363 + describe("getDaysInMonth helper", () => { 364 + it("should return correct days for February (non-leap year)", () => { 365 + const days = getDaysInMonth(2026, 1); // Passing 1 for February (0-indexed) 366 + expect(days).toBe(28); 367 + }); 368 + 369 + it("should return correct days for February (leap year)", () => { 370 + const days = getDaysInMonth(2028, 1); // Passing 1 for February (0-indexed) 371 + expect(days).toBe(29); 372 + }); 373 + 374 + it("should return correct days for January", () => { 375 + const days = getDaysInMonth(2026, 0); // Passing 0 for January (0-indexed) 376 + expect(days).toBe(31); 377 + }); 378 + 379 + it("should return correct days for April (30 days)", () => { 380 + const days = getDaysInMonth(2026, 3); // Passing 3 for April (0-indexed) 381 + expect(days).toBe(30); 382 + }); 383 + 384 + it("should return correct days for December", () => { 385 + const days = getDaysInMonth(2026, 11); // Passing 11 for December (0-indexed) 386 + expect(days).toBe(31); 387 + }); 388 + }); 389 + 390 + describe("nextRun with month boundaries", () => { 391 + it("should correctly handle end of February in non-leap year", () => { 392 + const from = new Date("2026-02-28T23:59:00Z"); 393 + const next = nextRun("0 0 * * *", { from }); 394 + 395 + expect(next.toISOString()).toBe("2026-03-01T00:00:00.000Z"); 396 + }); 397 + 398 + it("should correctly handle end of February in leap year", () => { 399 + const from = new Date("2028-02-28T23:59:00Z"); 400 + const next = nextRun("0 0 * * *", { from }); 401 + 402 + expect(next.toISOString()).toBe("2028-02-29T00:00:00.000Z"); 403 + }); 404 + 405 + it("should correctly handle Feb 29 to March 1 in leap year", () => { 406 + const from = new Date("2028-02-29T23:59:00Z"); 407 + const next = nextRun("0 0 * * *", { from }); 408 + 409 + expect(next.toISOString()).toBe("2028-03-01T00:00:00.000Z"); 410 + }); 411 + 412 + it("should correctly handle end of January", () => { 413 + const from = new Date("2026-01-31T23:59:00Z"); 414 + const next = nextRun("0 0 * * *", { from }); 415 + 416 + expect(next.toISOString()).toBe("2026-02-01T00:00:00.000Z"); 417 + }); 418 + 419 + it("should correctly handle end of April (30 days)", () => { 420 + const from = new Date("2026-04-30T23:59:00Z"); 421 + const next = nextRun("0 0 * * *", { from }); 422 + 423 + expect(next.toISOString()).toBe("2026-05-01T00:00:00.000Z"); 424 + }); 425 + 426 + it("should correctly handle cron on 31st of month", () => { 427 + const from = new Date("2026-01-15T00:00:00Z"); 428 + const next = nextRun("0 12 31 * *", { from }); 429 + 430 + expect(next.toISOString()).toBe("2026-01-31T12:00:00.000Z"); 431 + }); 432 + 433 + it("should skip months without 31 days when cron specifies day 31", () => { 434 + const from = new Date("2026-01-31T13:00:00Z"); // After Jan 31 noon 435 + const next = nextRun("0 12 31 * *", { from }); 436 + 437 + // February doesn't have 31 days, should skip to March 31 438 + expect(next.toISOString()).toBe("2026-03-31T12:00:00.000Z"); 439 + }); 440 + 441 + it("should skip February in non-leap years when cron specifies day 29", () => { 442 + const from = new Date("2026-01-29T13:00:00Z"); // After Jan 29 noon 443 + const next = nextRun("0 12 29 * *", { from }); 444 + 445 + // February 2026 doesn't have 29 days (not a leap year), should skip to March 29 446 + expect(next.toISOString()).toBe("2026-03-29T12:00:00.000Z"); 447 + }); 448 + 449 + it("should include February in leap years when cron specifies day 29", () => { 450 + const from = new Date("2028-01-29T13:00:00Z"); // After Jan 29 noon 451 + const next = nextRun("0 12 29 * *", { from }); 452 + 453 + // February 2028 has 29 days (leap year), should run on Feb 29 454 + expect(next.toISOString()).toBe("2028-02-29T12:00:00.000Z"); 455 + }); 456 + 457 + it("should skip February when cron specifies day 30", () => { 458 + const from = new Date("2026-01-30T13:00:00Z"); // After Jan 30 noon 459 + const next = nextRun("0 12 30 * *", { from }); 460 + 461 + // February never has 30 days, should skip to March 30 462 + expect(next.toISOString()).toBe("2026-03-30T12:00:00.000Z"); 463 + }); 464 + }); 465 + 466 + describe("previousRun with month boundaries", () => { 467 + it("should correctly handle beginning of March (non-leap year)", () => { 468 + const from = new Date("2026-03-01T00:01:00Z"); 469 + const prev = previousRun("0 0 * * *", { from }); 470 + 471 + expect(prev.toISOString()).toBe("2026-03-01T00:00:00.000Z"); 472 + }); 473 + 474 + it("should correctly handle beginning of March (leap year)", () => { 475 + const from = new Date("2028-03-01T00:01:00Z"); 476 + const prev = previousRun("0 0 * * *", { from }); 477 + 478 + expect(prev.toISOString()).toBe("2028-03-01T00:00:00.000Z"); 479 + }); 480 + }); 481 + 482 + describe("isMatch with specific months", () => { 483 + it("should match dates in February correctly", () => { 484 + const febDate = new Date("2026-02-15T10:00:00Z"); 485 + expect(isMatch("0 10 * 2 *", febDate)).toBe(true); 486 + 487 + const janDate = new Date("2026-01-15T10:00:00Z"); 488 + expect(isMatch("0 10 * 2 *", janDate)).toBe(false); 489 + }); 490 + 491 + it("should match dates in December correctly", () => { 492 + const decDate = new Date("2026-12-25T10:00:00Z"); 493 + expect(isMatch("0 10 * 12 *", decDate)).toBe(true); 494 + 495 + const novDate = new Date("2026-11-25T10:00:00Z"); 496 + expect(isMatch("0 10 * 12 *", novDate)).toBe(false); 497 + }); 498 + 499 + it("should match date ranges correctly", () => { 500 + const juneDate = new Date("2026-06-15T10:00:00Z"); 501 + const julyDate = new Date("2026-07-15T10:00:00Z"); 502 + const augDate = new Date("2026-08-15T10:00:00Z"); 503 + const mayDate = new Date("2026-05-15T10:00:00Z"); 504 + 505 + expect(isMatch("0 10 * 6-8 *", juneDate)).toBe(true); 506 + expect(isMatch("0 10 * 6-8 *", julyDate)).toBe(true); 507 + expect(isMatch("0 10 * 6-8 *", augDate)).toBe(true); 508 + expect(isMatch("0 10 * 6-8 *", mayDate)).toBe(false); 509 + }); 510 + }); 511 + }); 512 + 513 + describe("Past dates support", () => { 514 + describe("nextRun with past dates", () => { 515 + it("should calculate next run from a date in 2020", () => { 516 + const from = new Date("2020-06-15T14:30:00Z"); 517 + const next = nextRun("0 15 * * *", { from }); 518 + 519 + expect(next.getUTCHours()).toBe(15); 520 + expect(next.getUTCMinutes()).toBe(0); 521 + expect(next.getUTCFullYear()).toBe(2020); 522 + expect(next.getUTCMonth()).toBe(5); // June 523 + expect(next.getUTCDate()).toBe(15); 524 + }); 525 + it("should handle year boundaries in the past", () => { 526 + const from = new Date("2019-12-31T23:00:00Z"); 527 + const next = nextRun("0 0 1 1 *", { from }); 528 + 529 + expect(next.getUTCFullYear()).toBe(2020); 530 + expect(next.getUTCMonth()).toBe(0); // January 531 + expect(next.getUTCDate()).toBe(1); 532 + }); 533 + 534 + it("should handle leap year in the past (2020)", () => { 535 + const from = new Date("2020-02-28T12:00:00Z"); 536 + const next = nextRun("0 9 29 2 *", { from }); 537 + 538 + expect(next.getUTCFullYear()).toBe(2020); 539 + expect(next.getUTCMonth()).toBe(1); // February 540 + expect(next.getUTCDate()).toBe(29); 541 + }); 542 + 543 + it("should work with timezones for past dates", () => { 544 + const from = new Date("2021-03-15T14:00:00Z"); 545 + const next = nextRun("0 10 * * *", { 546 + from, 547 + timezone: "America/New_York", 548 + }); 549 + 550 + // March 2021: DST started March 14, so EDT (UTC-4) 551 + expect(next.getUTCHours()).toBe(14); // 10 AM EDT = 2 PM UTC 552 + expect(next.getTime()).toBeGreaterThan(from.getTime()); 553 + }); 554 + }); 555 + 556 + describe("previousRun with past dates", () => { 557 + it("should calculate previous run from a date in 2018", () => { 558 + const from = new Date("2018-08-20T14:30:00Z"); 559 + const prev = previousRun("0 9 * * *", { from }); 560 + 561 + expect(prev.getUTCHours()).toBe(9); 562 + expect(prev.getUTCMinutes()).toBe(0); 563 + expect(prev.getUTCFullYear()).toBe(2018); 564 + expect(prev.getUTCMonth()).toBe(7); // August 565 + expect(prev.getUTCDate()).toBe(20); 566 + }); 567 + 568 + it("should handle month boundaries in the past", () => { 569 + const from = new Date("2019-03-01T08:00:00Z"); 570 + const prev = previousRun("0 9 * * *", { from }); 571 + 572 + expect(prev.getUTCFullYear()).toBe(2019); 573 + expect(prev.getUTCMonth()).toBe(1); // February 574 + expect(prev.getUTCDate()).toBe(28); // 2019 is not a leap year 575 + }); 576 + }); 577 + 578 + describe("isMatch with past dates", () => { 579 + it("should match dates from 2015", () => { 580 + const date = new Date("2015-07-04T09:00:00Z"); 581 + expect(isMatch("0 9 * * *", date)).toBe(true); 582 + expect(isMatch("0 10 * * *", date)).toBe(false); 583 + }); 584 + 585 + it("should match weekdays correctly for past dates", () => { 586 + // July 4, 2015 was a Saturday (day 6) 587 + const date = new Date("2015-07-04T09:00:00Z"); 588 + expect(isMatch("0 9 * * 6", date)).toBe(true); 589 + expect(isMatch("0 9 * * 0", date)).toBe(false); 590 + }); 591 + 592 + it("should match specific months in the past", () => { 593 + const date = new Date("2017-12-25T10:00:00Z"); 594 + expect(isMatch("0 10 * 12 *", date)).toBe(true); 595 + expect(isMatch("0 10 * 11 *", date)).toBe(false); 596 + }); 597 + }); 598 + 599 + describe("nextRuns with past dates", () => { 600 + it("should generate multiple runs from a past date", () => { 601 + const from = new Date("2019-01-01T00:00:00Z"); 602 + const runs = nextRuns("0 0 * * *", 5, { from }); 603 + 604 + expect(runs).toHaveLength(5); 605 + expect(runs[0].getUTCFullYear()).toBe(2019); 606 + expect(runs[0].getUTCDate()).toBe(2); 607 + expect(runs[1].getUTCDate()).toBe(3); 608 + expect(runs[2].getUTCDate()).toBe(4); 609 + expect(runs[3].getUTCDate()).toBe(5); 610 + expect(runs[4].getUTCDate()).toBe(6); 611 + }); 612 + }); 613 + 614 + describe("DST transitions in past years", () => { 615 + it("should handle DST spring forward in 2019", () => { 616 + // March 10, 2019: DST started in US (2 AM -> 3 AM) 617 + const from = new Date("2019-03-09T00:00:00Z"); 618 + const next = nextRun("30 2 * * *", { 619 + from, 620 + timezone: "America/New_York", 621 + }); 622 + 623 + expect(next).toBeInstanceOf(Date); 624 + expect(next.getTime()).toBeGreaterThan(from.getTime()); 625 + }); 626 + 627 + it("should handle DST fall back in 2020", () => { 628 + // November 1, 2020: DST ended in US (2 AM -> 1 AM) 629 + const from = new Date("2020-10-31T00:00:00Z"); 630 + const next = nextRun("30 2 * * *", { 631 + from, 632 + timezone: "America/New_York", 633 + }); 634 + 635 + expect(next).toBeInstanceOf(Date); 636 + expect(next.getTime()).toBeGreaterThan(from.getTime()); 637 + }); 638 + }); 639 + 640 + describe("edge cases with very old dates", () => { 641 + it("should work with dates from 2000", () => { 642 + const from = new Date("2000-01-01T00:00:00Z"); 643 + const next = nextRun("0 12 * * *", { from }); 644 + 645 + expect(next.getUTCFullYear()).toBe(2000); 646 + expect(next.getUTCMonth()).toBe(0); 647 + expect(next.getUTCDate()).toBe(1); 648 + expect(next.getUTCHours()).toBe(12); 649 + }); 650 + 651 + it("should handle leap year 2000", () => { 652 + // 2000 was a leap year (divisible by 400) 653 + const from = new Date("2000-02-28T12:00:00Z"); 654 + const next = nextRun("0 9 29 2 *", { from }); 655 + 656 + expect(next.getUTCFullYear()).toBe(2000); 657 + expect(next.getUTCMonth()).toBe(1); // February 658 + expect(next.getUTCDate()).toBe(29); 659 + }); 660 + }); 661 + }); 662 + 663 + describe("Timezone handling", () => { 664 + let originalTZ: string | undefined; 665 + 666 + beforeEach(() => { 667 + originalTZ = process.env.TZ; 668 + }); 669 + 670 + afterEach(() => { 671 + if (originalTZ === undefined) { 672 + delete process.env.TZ; 673 + } else { 674 + process.env.TZ = originalTZ; 675 + } 676 + }); 677 + 678 + describe("system timezone independence", () => { 679 + it("should produce consistent results regardless of system timezone", () => { 680 + const cronExpr = "0 9 * * *"; 681 + const from = new Date("2026-02-05T00:00:00Z"); 682 + 683 + process.env.TZ = "UTC"; 684 + const resultUTC = nextRun(cronExpr, { from, timezone: "America/New_York" }); 685 + 686 + process.env.TZ = "Asia/Tokyo"; 687 + const resultTokyo = nextRun(cronExpr, { from, timezone: "America/New_York" }); 688 + 689 + process.env.TZ = "America/Los_Angeles"; 690 + const resultLA = nextRun(cronExpr, { from, timezone: "America/New_York" }); 691 + 692 + expect(resultUTC.getTime()).toBe(resultTokyo.getTime()); 693 + expect(resultUTC.getTime()).toBe(resultLA.getTime()); 694 + }); 695 + 696 + it("should handle timezone conversion correctly when system TZ differs", () => { 697 + const cronExpr = "0 12 * * *"; 698 + const from = new Date("2026-06-15T00:00:00Z"); 699 + 700 + const timezones = [ 701 + "UTC", 702 + "America/New_York", 703 + "Europe/London", 704 + "Asia/Tokyo", 705 + "Australia/Sydney", 706 + ]; 707 + const results: Date[] = []; 708 + 709 + for (const tz of timezones) { 710 + process.env.TZ = tz; 711 + const result = nextRun(cronExpr, { from, timezone: "America/Chicago" }); 712 + results.push(result); 713 + } 714 + 715 + const firstTime = results[0].getTime(); 716 + for (let i = 1; i < results.length; i++) { 717 + expect(results[i].getTime()).toBe(firstTime); 718 + } 719 + }); 720 + 721 + it("should work correctly without timezone option regardless of system TZ", () => { 722 + const cronExpr = "0 10 * * *"; 723 + const from = new Date("2026-03-01T00:00:00Z"); 724 + 725 + process.env.TZ = "UTC"; 726 + const resultUTC = nextRun(cronExpr, { from }); 727 + 728 + process.env.TZ = "Asia/Shanghai"; 729 + const resultShanghai = nextRun(cronExpr, { from }); 730 + 731 + expect(resultUTC.getTime()).toBe(resultShanghai.getTime()); 732 + }); 733 + }); 734 + 735 + describe("cross-timezone consistency", () => { 736 + it("should schedule next minute consistently across timezones", () => { 737 + const stockholm = nextRun("* * * * *", { timezone: "Europe/Stockholm" }); 738 + const newYork = nextRun("* * * * *", { timezone: "America/New_York" }); 739 + 740 + const diff = Math.abs(stockholm.getTime() - newYork.getTime()); 741 + expect(diff).toBeLessThan(2 * 60 * 1000); 742 + }); 743 + 744 + it("should maintain correct UTC offset between timezones", () => { 745 + const refTime = new Date("2026-09-01T00:00:00Z"); 746 + 747 + const stockholmNoon = nextRun("0 12 30 10 *", { 748 + from: refTime, 749 + timezone: "Europe/Stockholm", 750 + }); 751 + const newYorkNoon = nextRun("0 12 30 10 *", { 752 + from: refTime, 753 + timezone: "America/New_York", 754 + }); 755 + 756 + // Stockholm is UTC+1 (CET) and New York is UTC-4 (EDT) in October 2026 757 + const diffHours = (newYorkNoon.getTime() - stockholmNoon.getTime()) / 1000 / 3600; 758 + expect(diffHours).toBe(5); 759 + }); 760 + 761 + it("should produce correct UTC timestamps for local times", () => { 762 + const refTime = new Date("2026-02-08T12:00:00Z"); 763 + 764 + const stockholm = nextRun("0 23 * * 2", { 765 + from: refTime, 766 + timezone: "Europe/Stockholm", 767 + }); 768 + 769 + const newYork = nextRun("0 23 * * 2", { 770 + from: refTime, 771 + timezone: "America/New_York", 772 + }); 773 + 774 + // Stockholm is UTC+1 in February, so 11 PM = 10 PM UTC same day 775 + expect(stockholm.getUTCMonth()).toBe(1); 776 + expect(stockholm.getUTCDate()).toBe(10); 777 + expect(stockholm.getUTCHours()).toBe(22); 778 + expect(stockholm.getUTCFullYear()).toBe(2026); 779 + 780 + // New York is UTC-5 in February, so 11 PM = 4 AM UTC next day 781 + expect(newYork.getUTCMonth()).toBe(1); 782 + expect(newYork.getUTCDate()).toBe(11); 783 + expect(newYork.getUTCHours()).toBe(4); 784 + expect(newYork.getUTCFullYear()).toBe(2026); 785 + }); 786 + }); 787 + 788 + describe("DST transitions", () => { 789 + it("should handle spring forward transition (2 AM -> 3 AM)", () => { 790 + // March 8, 2026: clocks spring forward from 2 AM to 3 AM in US 791 + const from = new Date("2026-03-07T00:00:00Z"); 792 + const next = nextRun("30 2 * * *", { 793 + from, 794 + timezone: "America/New_York", 795 + }); 796 + 797 + expect(next).toBeInstanceOf(Date); 798 + expect(next.getTime()).toBeGreaterThan(from.getTime()); 799 + }); 800 + 801 + it("should handle fall back transition (2 AM occurs twice)", () => { 802 + // November 1, 2026: clocks fall back from 2 AM to 1 AM in US 803 + const from = new Date("2026-10-31T00:00:00Z"); 804 + const next = nextRun("30 2 * * *", { 805 + from, 806 + timezone: "America/New_York", 807 + }); 808 + 809 + expect(next).toBeInstanceOf(Date); 810 + expect(next.getTime()).toBeGreaterThan(from.getTime()); 811 + }); 812 + 813 + it("should handle UTC timezone correctly", () => { 814 + const from = new Date("2026-10-31T20:00:00Z"); 815 + const next = nextRun("0 12 * * *", { 816 + from, 817 + timezone: "Etc/UTC", 818 + }); 819 + 820 + expect(next.getUTCHours()).toBe(12); 821 + }); 822 + }); 823 + 824 + describe("year-long DST stability", () => { 825 + it("should handle 365 consecutive days at midnight in America/New_York", () => { 826 + const startDate = new Date("2026-01-01T12:00:00.000Z"); 827 + let current = new Date(startDate); 828 + 829 + const runs: Date[] = []; 830 + for (let i = 0; i < 365; i++) { 831 + current = nextRun("0 0 * * *", { 832 + from: current, 833 + timezone: "America/New_York", 834 + }); 835 + runs.push(new Date(current)); 836 + current = new Date(current.getTime() + 60000); 837 + } 838 + 839 + // Expected: 364 days between first and last run (365 runs spanning 364 days) 840 + const expectedLast = new Date(runs[0].getTime() + 364 * 24 * 60 * 60 * 1000); 841 + 842 + // Check timestamp is within DST tolerance 843 + const diff = Math.abs(runs[364].getTime() - expectedLast.getTime()); 844 + expect(diff).toBeLessThan(3 * 60 * 60 * 1000); // 3 hours max 845 + 846 + // Check we're on the correct calendar date (within 1 day) 847 + const lastRun = runs[364]; 848 + const expectedYear = expectedLast.getUTCFullYear(); 849 + const expectedMonth = expectedLast.getUTCMonth(); 850 + const expectedDay = expectedLast.getUTCDate(); 851 + 852 + expect(lastRun.getUTCFullYear()).toBe(expectedYear); 853 + expect(lastRun.getUTCMonth()).toBe(expectedMonth); 854 + expect(Math.abs(lastRun.getUTCDate() - expectedDay)).toBeLessThanOrEqual(1); 855 + }); 856 + 857 + it("should handle 365 consecutive days at 2:30 AM in America/New_York", () => { 858 + // 2:30 AM doesn't exist during spring DST transition 859 + const startDate = new Date("2026-01-01T12:00:00.000Z"); 860 + let current = new Date(startDate); 861 + 862 + const runs: Date[] = []; 863 + for (let i = 0; i < 365; i++) { 864 + current = nextRun("30 2 * * *", { 865 + from: current, 866 + timezone: "America/New_York", 867 + }); 868 + runs.push(new Date(current)); 869 + current = new Date(current.getTime() + 60000); 870 + } 871 + 872 + // Expected: 364 days between first and last run 873 + const expectedLast = new Date(runs[0].getTime() + 364 * 24 * 60 * 60 * 1000); 874 + 875 + // Check timestamp is within DST tolerance 876 + const diff = Math.abs(runs[364].getTime() - expectedLast.getTime()); 877 + expect(diff).toBeLessThan(3 * 60 * 60 * 1000); // 3 hours max 878 + 879 + // Check we're on the correct calendar date (within 1 day) 880 + const lastRun = runs[364]; 881 + const expectedYear = expectedLast.getUTCFullYear(); 882 + const expectedMonth = expectedLast.getUTCMonth(); 883 + const expectedDay = expectedLast.getUTCDate(); 884 + 885 + expect(lastRun.getUTCFullYear()).toBe(expectedYear); 886 + expect(lastRun.getUTCMonth()).toBe(expectedMonth); 887 + expect(Math.abs(lastRun.getUTCDate() - expectedDay)).toBeLessThanOrEqual(1); 888 + }); 889 + 890 + it("should handle 365 consecutive days at 1:30 AM in America/New_York", () => { 891 + const startDate = new Date("2026-01-01T12:00:00.000Z"); 892 + let current = new Date(startDate); 893 + 894 + const runs: Date[] = []; 895 + for (let i = 0; i < 365; i++) { 896 + current = nextRun("30 1 * * *", { 897 + from: current, 898 + timezone: "America/New_York", 899 + }); 900 + runs.push(new Date(current)); 901 + current = new Date(current.getTime() + 60000); 902 + } 903 + 904 + // Expected: 364 days between first and last run 905 + const expectedLast = new Date(runs[0].getTime() + 364 * 24 * 60 * 60 * 1000); 906 + 907 + // Check timestamp is within DST tolerance 908 + const diff = Math.abs(runs[364].getTime() - expectedLast.getTime()); 909 + expect(diff).toBeLessThan(3 * 60 * 60 * 1000); // 3 hours max 910 + 911 + // Check we're on the correct calendar date (within 1 day) 912 + const lastRun = runs[364]; 913 + const expectedYear = expectedLast.getUTCFullYear(); 914 + const expectedMonth = expectedLast.getUTCMonth(); 915 + const expectedDay = expectedLast.getUTCDate(); 916 + 917 + expect(lastRun.getUTCFullYear()).toBe(expectedYear); 918 + expect(lastRun.getUTCMonth()).toBe(expectedMonth); 919 + expect(Math.abs(lastRun.getUTCDate() - expectedDay)).toBeLessThanOrEqual(1); 920 + }); 921 + 922 + it("should handle 365 consecutive days at 2:30 AM in Europe/Berlin", () => { 923 + const startDate = new Date("2026-02-15T12:00:00.000Z"); 924 + let current = new Date(startDate); 925 + 926 + const runs: Date[] = []; 927 + for (let i = 0; i < 365; i++) { 928 + current = nextRun("30 2 * * *", { 929 + from: current, 930 + timezone: "Europe/Berlin", 931 + }); 932 + runs.push(new Date(current)); 933 + current = new Date(current.getTime() + 60000); 934 + } 935 + 936 + // Expected: 364 days between first and last run 937 + const expectedLast = new Date(runs[0].getTime() + 364 * 24 * 60 * 60 * 1000); 938 + 939 + // Check timestamp is within DST tolerance 940 + const diff = Math.abs(runs[364].getTime() - expectedLast.getTime()); 941 + expect(diff).toBeLessThan(3 * 60 * 60 * 1000); // 3 hours max 942 + 943 + // Check we're on the correct calendar date (within 1 day) 944 + const lastRun = runs[364]; 945 + const expectedYear = expectedLast.getUTCFullYear(); 946 + const expectedMonth = expectedLast.getUTCMonth(); 947 + const expectedDay = expectedLast.getUTCDate(); 948 + 949 + expect(lastRun.getUTCFullYear()).toBe(expectedYear); 950 + expect(lastRun.getUTCMonth()).toBe(expectedMonth); 951 + expect(Math.abs(lastRun.getUTCDate() - expectedDay)).toBeLessThanOrEqual(1); 952 + }); 953 + }); 954 + }); 955 + });
+54
test/timezone.test.ts
··· 1 + import { describe, it, expect } from "vitest"; 2 + import { convertFromTimezone, convertToTimezone } from "../src/timezone.js"; 3 + 4 + describe("Timezone Edge Cases - Deep Dive", () => { 5 + it("should verify convertToTimezone and convertFromTimezone are inverses", () => { 6 + const original = new Date("2026-06-15T10:30:00Z"); 7 + const timezone = "America/Chicago"; 8 + 9 + // Convert to timezone 10 + const converted = convertToTimezone(original, timezone); 11 + 12 + // Convert back 13 + const restored = convertFromTimezone(converted, timezone); 14 + 15 + // Should be identical 16 + expect(restored.getTime()).toBe(original.getTime()); 17 + }); 18 + 19 + it("should test multiple round-trips through different timezones", () => { 20 + const timezones = [ 21 + "America/New_York", 22 + "Europe/London", 23 + "Asia/Tokyo", 24 + "Australia/Sydney", 25 + "America/Los_Angeles", 26 + "Europe/Paris", 27 + ]; 28 + 29 + const original = new Date("2026-03-15T18:45:30Z"); 30 + 31 + for (const tz of timezones) { 32 + const converted = convertToTimezone(original, tz); 33 + const restored = convertFromTimezone(converted, tz); 34 + 35 + expect(restored.getTime()).toBe(original.getTime()); 36 + } 37 + }); 38 + 39 + it("should handle DST transitions correctly", () => { 40 + // Test around DST transition in New York (March 8, 2026, 2 AM -> 3 AM) 41 + const beforeDST = new Date("2026-03-08T06:00:00Z"); // 1 AM EST (before transition) 42 + const afterDST = new Date("2026-03-08T08:00:00Z"); // 4 AM EDT (after transition) 43 + // Skip the transition hour itself (2-3 AM doesn't exist) 44 + 45 + const timezone = "America/New_York"; 46 + 47 + for (const date of [beforeDST, afterDST]) { 48 + const converted = convertToTimezone(date, timezone); 49 + const restored = convertFromTimezone(converted, timezone); 50 + 51 + expect(restored.getTime()).toBe(date.getTime()); 52 + } 53 + }); 54 + });
+19
tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ES2022", 4 + "module": "ESNext", 5 + "lib": ["ES2022"], 6 + "moduleResolution": "bundler", 7 + "strict": true, 8 + "skipLibCheck": true, 9 + "declaration": true, 10 + "declarationMap": true, 11 + "esModuleInterop": true, 12 + "resolveJsonModule": true, 13 + "isolatedModules": true, 14 + "outDir": "./dist", 15 + "rootDir": "./src" 16 + }, 17 + "include": ["src/**/*"], 18 + "exclude": ["node_modules", "dist", "test", "**/*.test.ts"] 19 + }
+12
tsdown.config.ts
··· 1 + import { defineConfig } from "tsdown"; 2 + 3 + export default defineConfig({ 4 + entry: ["src/index.ts"], 5 + format: ["cjs", "esm"], 6 + dts: true, 7 + sourcemap: true, 8 + clean: true, 9 + minify: true, 10 + treeshake: true, 11 + target: false, 12 + });
+15
vitest.config.ts
··· 1 + import { defineConfig } from "vitest/config"; 2 + 3 + export default defineConfig({ 4 + test: { 5 + globals: true, 6 + environment: "node", 7 + include: ["test/**/*.test.ts"], 8 + coverage: { 9 + provider: "v8", 10 + reporter: ["text", "json", "html"], 11 + include: ["src/**/*.ts"], 12 + exclude: ["**/*.test.ts", "**/*.config.ts", "dist/**"], 13 + }, 14 + }, 15 + });