Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,14 @@ jobs:
run: npm ci

- name: Generate windowsZones (strict)
run: node build/update-windows-zones.mjs
run: node build/update-windows-zones.js
env:
CI: true

- name: Build CommonJS entry
run: npm run build:cjs

- name: Unit Tests
run: npx mocha --timeout 8000
run: npx mocha --extension js --timeout 8000
env:
CI: true
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
node_modules
.idea
.DS_Store

# Generated CommonJS entry (built from node-ical.js via `npm run build:cjs`)
node-ical.cjs
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ node-ical is available on npm:
npm install node-ical
```

## Module formats
node-ical provides an ESM-first entry point while keeping CommonJS compatibility.

```javascript
// ESM
import ical from 'node-ical';
```

```javascript
// CommonJS
const ical = require('node-ical');
```

## API
The API has now been broken into three sections:
- [sync](#sync)
Expand All @@ -29,8 +42,8 @@ All functions will either return a promise for `async/await` or use a callback i

`autodetect` provides a mix of both for backwards compatibility with older node-ical applications.

All API functions are documented using JSDoc in the [node-ical.js](node-ical.js) file.
This allows for IDE hinting!
All API functions are documented in the runtime entry files and exposed through the bundled typings in [node-ical.d.ts](node-ical.d.ts).
This allows for IDE hinting in both CommonJS and ESM projects.

### sync
```javascript
Expand Down Expand Up @@ -197,9 +210,9 @@ Fetch the specified URL using the native fetch API (```options``` are passed to

#### Example: Print list of upcoming node conferences

See [`examples/example.mjs`](./examples/example.mjs) for a full example script.
See [`examples/example.js`](./examples/example.js) for a full example script.

> **Note:** This snippet uses `import` and top-level `await` (ESM). Save it as a `.mjs` file, or add `"type": "module"` to your `package.json`.
> **Note:** This snippet uses `import` and top-level `await` (ESM). Save it as a `.mjs` file, or add `"type": "module"` to your `package.json`. If you prefer CommonJS in your own project, keep using `require('node-ical')` from a `.cjs` file.

```javascript
import ical from 'node-ical';
Expand Down
6 changes: 3 additions & 3 deletions build/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This folder contains data and scripts used to generate `windowsZones.json`, the

## Files

- `update-windows-zones.mjs`: Node-only updater that fetches the upstream CLDR `windowsZones.xml` and regenerates `windowsZones.json` using fast-xml-parser.
- `update-windows-zones.js`: Node-only updater that fetches the upstream CLDR `windowsZones.xml` and regenerates `windowsZones.json` using fast-xml-parser.
- `windowsZonesOld.json`: A curated map of legacy Windows display-name labels (the human-readable aliases used by various Outlook/Exchange/ICS exporters) to the canonical Windows time zone IDs (e.g., `"(UTC+03:00) Tbilisi" -> "Georgian Standard Time"`).

## Why `windowsZonesOld.json` exists
Expand All @@ -19,7 +19,7 @@ To keep `node-ical` resilient, we preserve a set of these legacy labels and map

## How generation works

1. `update-windows-zones.mjs` downloads CLDR `windowsZones.xml` and parses it directly.
1. `update-windows-zones.js` downloads CLDR `windowsZones.xml` and parses it directly.
2. It builds a `zoneTable` from CLDR, mapping Windows IDs to `{ iana: [primaryIana] }`.
3. It then loads `build/windowsZonesOld.json` and, for each legacy label (top-level key), looks up the canonical Windows ID (value) and injects a mapping entry into the final `zoneTable` so that legacy labels resolve to the same IANA zone as the canonical ID.
4. The script writes `windowsZones.json` in a one-entry-per-line format with sorted keys for stable diffs.
Expand All @@ -38,7 +38,7 @@ npm run build:strict
# or
CI=true npm run build
# or (low-level)
node build/update-windows-zones.mjs --strict
node build/update-windows-zones.js --strict
```

Note: CI runs the generator in strict mode to catch unresolved aliases early in pull requests.
Expand Down
20 changes: 20 additions & 0 deletions build/build-cjs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Generate the CommonJS entry point (node-ical.cjs) from the ESM sources.
//
// ESM (*.js, type: module) is the single source of truth. This bundles
// node-ical.js into a single self-contained CommonJS file so that
// `require('node-ical')` keeps working, while runtime dependencies stay
// external `require()` calls.
import {build} from 'esbuild';

await build({
entryPoints: ['node-ical.js'],
outfile: 'node-ical.cjs',
bundle: true,
platform: 'node',
format: 'cjs',
target: 'node20',
// Keep node_modules dependencies as require() calls; only bundle our own code
// (and inline windowsZones.json, which is imported relatively).
packages: 'external',
logLevel: 'info',
});
37 changes: 10 additions & 27 deletions build/update-windows-zones.mjs → build/update-windows-zones.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Update windowsZones.json from the upstream CLDR windowsZones.xml using fast-xml-parser.
// This replaces the old xml-js CLI + shell script pipeline with a single cross-platform Node script.

import https from 'node:https';
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
Expand All @@ -14,34 +13,18 @@ const __dirname = path.dirname(__filename);
const SOURCE_URL = 'https://raw.githubusercontent.com/unicode-org/cldr/master/common/supplemental/windowsZones.xml';
const OLD_MAP_PATH = path.join(__dirname, 'windowsZonesOld.json');
const OUTPUT_PATH = path.join(__dirname, '..', 'windowsZones.json');
const FETCH_TIMEOUT_MS = 30_000;

function fetchText(url) {
return new Promise((resolve, reject) => {
https
.get(url, response => {
if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
// Follow redirect
fetchText(response.headers.location).then(resolve).catch(reject);
return;
}

if (response.statusCode !== 200) {
reject(new Error(`HTTP ${response.statusCode} when fetching ${url}`));
response.resume();
return;
}

let data = '';
response.setEncoding('utf8');
response.on('data', chunk => {
data += chunk;
});
response.on('end', () => {
resolve(data);
});
})
.on('error', reject);
async function fetchText(url) {
const response = await fetch(url, {
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
});

if (!response.ok) {
throw new Error(`HTTP ${response.status} when fetching ${url}`);
}

return response.text();
}

function toArray(value) {
Expand Down
7 changes: 5 additions & 2 deletions examples/example-rrule-basic.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
* RRULE within a fixed window, and print the resulting instances.
*/

const path = require('node:path');
const ical = require('../node-ical.js');
import path from 'node:path';
import {fileURLToPath} from 'node:url';
import ical from 'node-ical';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// Load the basic example calendar that contains one recurring event.
const data = ical.parseFile(path.join(__dirname, 'example-rrule-basic.ics'));
Expand Down
13 changes: 6 additions & 7 deletions examples/example-rrule-datefns.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,12 @@
* fixed window (here: calendar year 2017) keeps expansion finite and practical.
*/

const path = require('node:path');
const {
format,
differenceInMilliseconds,
parseISO,
} = require('date-fns');
const ical = require('../node-ical.js');
import path from 'node:path';
import {fileURLToPath} from 'node:url';
import {differenceInMilliseconds, format, parseISO} from 'date-fns';
import ical from 'node-ical';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// Load an example iCal file with various recurring events.
const data = ical.parseFile(path.join(__dirname, 'example-rrule.ics'));
Expand Down
15 changes: 9 additions & 6 deletions examples/example-rrule-dayjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@
* fixed window (here: calendar year 2017) keeps expansion finite and practical.
*/

const path = require('node:path');
const dayjs = require('dayjs');
const utc = require('dayjs/plugin/utc');
const duration = require('dayjs/plugin/duration');
const localizedFormat = require('dayjs/plugin/localizedFormat');
const ical = require('../node-ical.js');
import path from 'node:path';
import {fileURLToPath} from 'node:url';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration.js';
import localizedFormat from 'dayjs/plugin/localizedFormat.js';
import utc from 'dayjs/plugin/utc.js';
import ical from 'node-ical';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// Extend Day.js with plugins for timezone and duration support
dayjs.extend(utc);
Expand Down
9 changes: 6 additions & 3 deletions examples/example-rrule-luxon.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@
* fixed window (here: calendar year 2017) keeps expansion finite and practical.
*/

const path = require('node:path');
const {DateTime} = require('luxon');
const ical = require('../node-ical.js');
import path from 'node:path';
import {fileURLToPath} from 'node:url';
import {DateTime} from 'luxon';
import ical from 'node-ical';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// Load an example iCal file with various recurring events.
const data = ical.parseFile(path.join(__dirname, 'example-rrule.ics'));
Expand Down
9 changes: 6 additions & 3 deletions examples/example-rrule-moment.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@
* fixed window (here: calendar year 2017) keeps expansion finite and practical.
*/

const path = require('node:path');
const moment = require('moment-timezone');
const ical = require('../node-ical.js');
import path from 'node:path';
import {fileURLToPath} from 'node:url';
import moment from 'moment-timezone';
import ical from 'node-ical';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// Load an example iCal file with various recurring events.
const data = ical.parseFile(path.join(__dirname, 'example-rrule.ics'));
Expand Down
7 changes: 5 additions & 2 deletions examples/example-rrule-vanilla.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@
* fixed window (here: calendar year 2017) keeps expansion finite and practical.
*/

const path = require('node:path');
const ical = require('../node-ical.js');
import path from 'node:path';
import {fileURLToPath} from 'node:url';
import ical from 'node-ical';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// Helper function to format duration from milliseconds to hours:minutes
function formatDuration(ms) {
Expand Down
2 changes: 1 addition & 1 deletion examples/example.mjs → examples/example.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* resulting VEVENT entries using the Promise-based API.
*/

import ical from '../node-ical.js';
import ical from 'node-ical';

const url = 'https://raw.githubusercontent.com/jens-maus/node-ical/master/test/fixtures/festival-multiday-rrule.ics';

Expand Down
Loading