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.

Fix tsconfig for cli tool, tests (#33)

* separate tsconfig for cli tool

* cli tests

* move tests after build

* remove cli test for workerd

* fix deno,bun,workerd tests

authored by

Kasparas Bilkis and committed by
GitHub
c855e96c b379ae60

+144 -71
+5 -5
.github/workflows/ci.yml
··· 38 38 - name: Type check 39 39 run: pnpm run typecheck 40 40 41 - - name: Run tests 42 - run: pnpm run test 43 - 44 41 - name: Build 45 42 run: pnpm run build 46 43 44 + - name: Run tests 45 + run: pnpm run test 46 + 47 47 # Test with Deno 48 48 test-deno: 49 49 name: Test on Deno ··· 60 60 run: deno install 61 61 62 62 - name: Run tests with Deno 63 - run: deno run -A npm:vitest run 63 + run: deno run -A npm:vitest run --exclude test/cli.test.ts 64 64 65 65 # Test with Bun 66 66 test-bun: ··· 78 78 run: bun install --frozen-lockfile 79 79 80 80 - name: Run tests 81 - run: bun test 81 + run: bun run test:noCli 82 82 83 83 # Test with Cloudflare Workers runtime (workerd via @cloudflare/vitest-pool-workers) 84 84 test-workerd:
+2 -3
README.md
··· 7 7 [![CI](https://github.com/kbilkis/cron-fast/actions/workflows/ci.yml/badge.svg)](https://github.com/kbilkis/cron-fast/actions/workflows/ci.yml) 8 8 [![codecov](https://codecov.io/github/kbilkis/cron-fast/graph/badge.svg)](https://codecov.io/github/kbilkis/cron-fast) 9 9 [![npm bundle size](https://deno.bundlejs.com/badge?q=cron-fast)](https://bundlejs.com/?q=cron-fast) 10 - 11 10 [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 12 11 13 12 **10x+ faster than the alternatives. 3.5KB gzipped. Zero dependencies.** ··· 38 37 39 38 cron-fast is designed for speed and efficiency. Here's how it compares to popular alternatives: 40 39 41 - > Tested with cron-fast v3.1.0, croner v10.0.1, cron-parser v5.5.0, cron-schedule v6.0.0 on Node.js v22.18.0 40 + > Tested with cron-fast v3.1.1, croner v10.0.1, cron-parser v5.5.0, cron-schedule v6.0.0 on Node.js v22.18.0 42 41 43 42 | Operation | cron-fast | croner | cron-parser | cron-schedule | 44 43 | ------------ | --------------- | --------- | ----------- | ------------- | ··· 215 214 216 215 ## Bundle Size 217 216 218 - cron-fast is extremely lightweight and fully tree-shakeable. Here are the actual bundle sizes for different import scenarios (tested with v3.1.0): 217 + cron-fast is extremely lightweight and fully tree-shakeable. Here are the actual bundle sizes for different import scenarios (tested with v3.1.1): 219 218 220 219 | Import | Raw | Minified | Gzipped | 221 220 | ------------------------------------------------------ | -------- | -------- | ----------- |
+1 -1
docs/benchmark-comparison-bun.md
··· 1 1 # Benchmark 2 2 3 - > Tested with bun v1.3.9, cron-fast v3.1.0, croner v10.0.1, cron-parser v5.5.0, cron-schedule v6.0.0, cron-validate v1.5.3 3 + > Tested with bun v1.3.9, cron-fast v3.1.1, croner v10.0.1, cron-parser v5.5.0, cron-schedule v6.0.0, cron-validate v1.5.3 4 4 > Tested on MacBook M1 pro 5 5 6 6 ## Performance Benchmarks
+1 -1
docs/benchmark-comparison-deno.md
··· 1 1 # Benchmark 2 2 3 - > Tested with deno v2.6.8, cron-fast v3.1.0, croner v10.0.1, cron-parser v5.5.0, cron-schedule v6.0.0, cron-validate v1.5.3 3 + > Tested with deno v2.6.8, cron-fast v3.1.1, croner v10.0.1, cron-parser v5.5.0, cron-schedule v6.0.0, cron-validate v1.5.3 4 4 > Tested on MacBook M1 pro 5 5 6 6 ## Performance Benchmarks
+1 -1
docs/benchmark-comparison-node.md
··· 1 1 # Benchmark 2 2 3 - > Tested with node v22.18.0, cron-fast v3.1.0, croner v10.0.1, cron-parser v5.5.0, cron-schedule v6.0.0, cron-validate v1.5.3 3 + > Tested with node v22.18.0, cron-fast v3.1.1, croner v10.0.1, cron-parser v5.5.0, cron-schedule v6.0.0, cron-validate v1.5.3 4 4 > Tested on MacBook M1 pro 5 5 6 6 ## Performance Benchmarks
+1 -1
jsr.json
··· 1 1 { 2 2 "name": "@kbilkis/cron-fast", 3 - "version": "3.1.0", 3 + "version": "3.1.1", 4 4 "description": "Fast and tiny JavaScript/TypeScript cron parser with timezone support - works in Node.js, Deno, Bun, Cloudflare Workers, and browsers. Zero dependencies.", 5 5 "keywords": [ 6 6 "javascript",
+5 -4
package.json
··· 1 1 { 2 2 "name": "cron-fast", 3 - "version": "3.1.0", 3 + "version": "3.1.1", 4 4 "description": "Fast and tiny JavaScript/TypeScript cron parser with timezone support - works in Node.js, Deno, Bun, Cloudflare Workers, and browsers. Zero dependencies.", 5 5 "keywords": [ 6 6 "browser", ··· 72 72 "test": "vitest run", 73 73 "test:watch": "vitest", 74 74 "test:ui": "vitest --ui", 75 - "test:coverage": "vitest run --coverage", 76 - "test:workerd": "vitest run --config vitest.workerd.config.ts", 75 + "test:noCli": "vitest run --exclude test/cli.test.ts", 76 + "test:coverage": "pnpm test:noCli --coverage", 77 + "test:workerd": "pnpm test:noCli --config vitest.workerd.config.ts", 77 78 "lint": "oxlint", 78 79 "lint:fix": "oxlint --fix", 79 80 "fmt": "oxfmt", 80 81 "fmt:check": "oxfmt --check", 81 - "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json", 82 + "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json && tsc --noEmit -p tsconfig.cli.json", 82 83 "benchmark": "tsx scripts/benchmark.ts", 83 84 "benchmark:deno": "deno run --allow-read --allow-env --allow-write --unstable-sloppy-imports scripts/benchmark.ts", 84 85 "benchmark:bun": "bun run scripts/benchmark.ts",
+117
test/cli.test.ts
··· 1 + import { describe, it, expect } from "vitest"; 2 + import { execSync } from "node:child_process"; 3 + 4 + const cli = "node dist/cli.cjs"; 5 + 6 + function run(args: string): { stdout: string; stderr: string; exitCode: number } { 7 + try { 8 + const stdout = execSync(`${cli} ${args}`, { encoding: "utf-8", stdio: "pipe" }); 9 + return { stdout: stdout.trim(), stderr: "", exitCode: 0 }; 10 + } catch (e: any) { 11 + return { 12 + stdout: (e.stdout ?? "").toString().trim(), 13 + stderr: (e.stderr ?? "").toString().trim(), 14 + exitCode: e.status ?? 1, 15 + }; 16 + } 17 + } 18 + 19 + describe("cli", () => { 20 + it("should show help", () => { 21 + const result = run("--help"); 22 + expect(result.exitCode).toBe(0); 23 + expect(result.stdout).toContain("Usage: cron-fast"); 24 + expect(result.stdout).toContain("--next"); 25 + expect(result.stdout).toContain("--describe"); 26 + expect(result.stdout).toContain("--validate"); 27 + }); 28 + 29 + it("should show help when called with no args", () => { 30 + const result = run(""); 31 + expect(result.exitCode).toBe(0); 32 + expect(result.stdout).toContain("Usage: cron-fast"); 33 + }); 34 + 35 + it("should show help with --help flag", () => { 36 + const result = run("--help"); 37 + expect(result.exitCode).toBe(0); 38 + expect(result.stdout).toContain("--next"); 39 + expect(result.stdout).toContain("--validate"); 40 + }); 41 + 42 + it("should reject an invalid expression", () => { 43 + const result = run('"invalid" --next 1'); 44 + expect(result.exitCode).toBe(1); 45 + }); 46 + 47 + it("should validate a valid expression (exit 0)", () => { 48 + const result = run('"0 9 * * *" --validate'); 49 + expect(result.exitCode).toBe(0); 50 + expect(result.stdout).toBe(""); 51 + }); 52 + 53 + it("should reject an invalid expression (exit 1)", () => { 54 + const result = run('"invalid" --validate'); 55 + expect(result.exitCode).toBe(1); 56 + }); 57 + 58 + it("should describe an expression", () => { 59 + const result = run('"*/15 * * * *" --describe'); 60 + expect(result.exitCode).toBe(0); 61 + expect(result.stdout).toBe("Every 15 minutes"); 62 + }); 63 + 64 + it("should show next runs", () => { 65 + const result = run('"0 9 * * *" --next 3 --from 2026-03-15T10:00:00Z'); 66 + expect(result.exitCode).toBe(0); 67 + expect(result.stdout).toContain("2026-03-16"); 68 + expect(result.stdout).toContain("2026-03-17"); 69 + expect(result.stdout).toContain("2026-03-18"); 70 + }); 71 + 72 + it("should show previous runs", () => { 73 + const result = run('"0 12 * * *" --prev 3 --from 2026-03-16T10:00:00Z'); 74 + expect(result.exitCode).toBe(0); 75 + expect(result.stdout).toContain("2026-03-15"); 76 + expect(result.stdout).toContain("2026-03-14"); 77 + }); 78 + 79 + it("should match a date", () => { 80 + const result = run('"0 9 * * *" --match 2026-03-16T09:00:00Z'); 81 + expect(result.exitCode).toBe(0); 82 + expect(result.stdout).toBe("true"); 83 + }); 84 + 85 + it("should not match a wrong date", () => { 86 + const result = run('"0 9 * * *" --match 2026-03-16T10:00:00Z'); 87 + expect(result.exitCode).toBe(0); 88 + expect(result.stdout).toBe("false"); 89 + }); 90 + 91 + it("should output json", () => { 92 + const result = run('"0 9 * * *" --next 2 --json --from 2026-03-15T10:00:00Z'); 93 + expect(result.exitCode).toBe(0); 94 + const json = JSON.parse(result.stdout); 95 + expect(json.expression).toBe("0 9 * * *"); 96 + expect(json.runs).toHaveLength(2); 97 + expect(json.description).toContain("9:00 AM"); 98 + }); 99 + 100 + it("should handle timezone", () => { 101 + const result = run('"0 9 * * *" --next 1 --tz America/New_York --from 2026-03-15T10:00:00Z'); 102 + expect(result.exitCode).toBe(0); 103 + expect(result.stdout).toMatch(/\d{4}-\d{2}-\d{2}/); 104 + }); 105 + 106 + it("should error on invalid expression in next mode", () => { 107 + const result = run('"0 0 31 2 *" --next 1'); 108 + expect(result.exitCode).toBe(1); 109 + }); 110 + 111 + it("should json-error on invalid expression", () => { 112 + const result = run('"bad" --next 1 --json'); 113 + expect(result.exitCode).toBe(1); 114 + const json = JSON.parse(result.stdout); 115 + expect(json.error).toBeDefined(); 116 + }); 117 + });
-53
test/workerd.test.ts
··· 1 - import { describe, it, expect } from "vitest"; 2 - import { nextRun, previousRun, nextRuns, isValid, parse, isMatch } from "../src/index.js"; 3 - 4 - describe("workerd (Cloudflare Workers) compatibility", () => { 5 - it("should calculate next run time", () => { 6 - const next = nextRun("0 9 * * *"); 7 - expect(next).toBeInstanceOf(Date); 8 - expect(next.getTime()).toBeGreaterThan(Date.now()); 9 - }); 10 - 11 - it("should calculate previous run time", () => { 12 - const prev = previousRun("0 9 * * *"); 13 - expect(prev).toBeInstanceOf(Date); 14 - expect(prev.getTime()).toBeLessThan(Date.now()); 15 - }); 16 - 17 - it("should calculate multiple next runs", () => { 18 - const runs = nextRuns("0 9 * * *", 3); 19 - expect(Array.isArray(runs)).toBe(true); 20 - expect(runs.length).toBe(3); 21 - // Verify they're in chronological order 22 - const timestamps = runs.map((d) => d.getTime()); 23 - expect(timestamps[0]).toBeLessThan(timestamps[1]); 24 - expect(timestamps[1]).toBeLessThan(timestamps[2]); 25 - }); 26 - 27 - it("should validate cron expressions", () => { 28 - expect(isValid("0 9 * * *")).toBe(true); 29 - expect(isValid("invalid")).toBe(false); 30 - }); 31 - 32 - it("should parse cron expressions", () => { 33 - const parsed = parse("*/15 9-17 * * 1-5"); 34 - expect(parsed).toBeDefined(); 35 - expect(parsed.minute).toBeDefined(); 36 - expect(parsed.hour).toBeDefined(); 37 - expect(parsed.day).toBeDefined(); 38 - expect(parsed.month).toBeDefined(); 39 - expect(parsed.weekday).toBeDefined(); 40 - }); 41 - 42 - it("should match dates against cron expressions", () => { 43 - const date = new Date("2026-03-15T09:00:00Z"); 44 - expect(isMatch("0 9 * * *", date)).toBe(true); 45 - expect(isMatch("0 10 * * *", date)).toBe(false); 46 - }); 47 - 48 - it("should work with timezone options", () => { 49 - const next = nextRun("0 9 * * *", { timezone: "America/New_York" }); 50 - expect(next).toBeInstanceOf(Date); 51 - expect(next.getTime()).toBeGreaterThan(Date.now()); 52 - }); 53 - });
+9
tsconfig.cli.json
··· 1 + { 2 + "extends": "./tsconfig.json", 3 + "compilerOptions": { 4 + "noEmit": true, 5 + "lib": ["ES2022", "dom"], 6 + "types": ["node"] 7 + }, 8 + "include": ["src/cli.ts", "src/index.ts", "src/types.ts"] 9 + }
+1 -1
tsconfig.json
··· 15 15 "rootDir": "./src" 16 16 }, 17 17 "include": ["src/**/*"], 18 - "exclude": ["node_modules", "dist", "test"] 18 + "exclude": ["node_modules", "dist", "test", "src/cli.ts"] 19 19 }
+1 -1
vitest.config.ts
··· 9 9 provider: "v8", 10 10 reporter: ["text", "json", "html", "lcov"], 11 11 include: ["src/**/*.ts"], 12 - exclude: ["**/*.test.ts", "**/*.config.ts", "dist/**"], 12 + exclude: ["**/*.test.ts", "**/*.config.ts", "dist/**", "cli.ts"], 13 13 }, 14 14 }, 15 15 });