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
12 changes: 9 additions & 3 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ jobs:
- name: Install dependencies
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install
- name: Build
- name: Build (dual CJS + ESM)
run: yarn build
- name: Run CJS integration test
run: yarn test:integration:cjs
- name: Run ESM integration test
run: yarn test:integration:esm
- name: Run examples
run: node examples/example.js

Expand All @@ -54,11 +58,13 @@ jobs:
- name: Install dependencies
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install
- name: Build
- name: Build (dual CJS + ESM)
run: yarn build
- name: Setup Deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Run Deno integration test
- name: Run Deno CJS integration test
run: deno run --allow-read test/integration/deno.ts
- name: Run Deno ESM integration test
run: deno run --allow-read test/integration/deno-esm.ts
2 changes: 1 addition & 1 deletion examples/example.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const NtpTimeSync = require("../dist/index").NtpTimeSync;
const NtpTimeSync = require("../dist/cjs/index").NtpTimeSync;

(async function() {
const ntpInstance1 = NtpTimeSync.getInstance();
Expand Down
27 changes: 25 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,24 @@
"name": "ntp-time-sync",
"version": "0.5.0",
"description": "Fetches the current time from NTP servers and returns offset information",
"main": "dist/index.js",
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/cjs/index.d.ts",
"type": "commonjs",
"exports": {
".": {
"import": {
"types": "./dist/esm/index.d.ts",
"default": "./dist/esm/index.js"
},
"require": {
"types": "./dist/cjs/index.d.ts",
"default": "./dist/cjs/index.js"
}
},
"./package.json": "./package.json"
},
"sideEffects": false,
"dependencies": {
"ntp-packet-parser": "^0.6.0"
},
Expand All @@ -15,7 +31,14 @@
},
"scripts": {
"prepublishOnly": "yarn prettier:lint && yarn build",
"build": "tsc",
"clean": "rm -rf dist",
"build:cjs": "tsc -p tsconfig.cjs.json && node scripts/write-package-type.mjs cjs",
"build:esm": "tsc -p tsconfig.esm.json && node scripts/write-package-type.mjs esm",
"build": "yarn build:cjs && yarn build:esm",
"test:integration:cjs": "node test/integration/cjs.cjs",
"test:integration:esm": "node test/integration/esm.mjs",
"test:integration:deno": "deno run --allow-read test/integration/deno.ts && deno run --allow-read test/integration/deno-esm.ts",
"test:integration": "yarn test:integration:cjs && yarn test:integration:esm",
"prettier": "prettier --write src/**/*.ts",
"prettier:lint": "prettier --list-different src/**/*.ts"
},
Expand Down
25 changes: 25 additions & 0 deletions scripts/write-package-type.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Writes the appropriate `package.json` marker into dist/{cjs,esm} so Node's
// module resolver treats the emitted .js files with the correct format.
//
// Usage: node scripts/write-package-type.mjs <cjs|esm>

import { mkdirSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const target = process.argv[2];
if (target !== "cjs" && target !== "esm") {
console.error(`Usage: node scripts/write-package-type.mjs <cjs|esm>`);
process.exit(1);
}

const type = target === "cjs" ? "commonjs" : "module";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const outPath = resolve(__dirname, "..", "dist", target, "package.json");

mkdirSync(dirname(outPath), { recursive: true });
writeFileSync(outPath, JSON.stringify({ type }) + "\n");

console.log(`wrote ${outPath} → {"type":"${type}"}`);
6 changes: 3 additions & 3 deletions src/NtpTimeSync.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"use strict";

import * as dgram from "dgram";
import * as dgram from "node:dgram";
import { NtpPacket, NtpPacketParser } from "ntp-packet-parser";
import { NtpTimeResult } from "./NtpTimeResult";
import { RecursivePartial } from "./RecursivePartial";
import { NtpTimeResult } from "./NtpTimeResult.js";
import { RecursivePartial } from "./RecursivePartial.js";

let singleton: NtpTimeSync | undefined;
let lastPoll: number | undefined;
Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from "./NtpTimeResult";
export * from "./NtpTimeSync";
export * from "./NtpTimeResult.js";
export * from "./NtpTimeSync.js";
45 changes: 45 additions & 0 deletions test/integration/cjs.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// CJS integration test — verifies the dual build's CommonJS output works for plain `require()` consumers.
// Run: node test/integration/cjs.cjs

"use strict";

const assert = require("node:assert/strict");

const mod = require("../../dist/cjs/index.js");
const { NtpTimeSync, NtpTimeSyncDefaultOptions } = mod;

assert.equal(typeof NtpTimeSync, "function", "NtpTimeSync should be exported as a class");
assert.ok(NtpTimeSyncDefaultOptions && typeof NtpTimeSyncDefaultOptions === "object", "defaults should be exported");

// Instantiate with nested options to exercise recursiveResolveOptions → isPlainObject
const instance = new NtpTimeSync({
sampleCount: 4,
ntpDefaults: {
minPoll: 4,
maxPoll: 10,
},
});
assert.ok(instance instanceof NtpTimeSync, "constructor should return an NtpTimeSync instance");

// Verify singleton
const a = NtpTimeSync.getInstance();
const b = NtpTimeSync.getInstance();
assert.equal(a, b, "getInstance must return the same singleton");
assert.ok(a instanceof NtpTimeSync, "singleton must be an NtpTimeSync instance");

// Verify API surface (shape only — no network calls)
assert.equal(typeof a.getTime, "function", "getTime must be a method");
assert.equal(typeof a.now, "function", "now must be a method");
assert.equal(typeof a.getNetworkTime, "function", "getNetworkTime must be a method");

// Verify no __proto__ pollution in resolved options
const opts = /** @type {any} */ (a).options;
assert.ok(opts, "instance.options must be defined");
assert.equal(Object.getPrototypeOf(opts.ntpDefaults), Object.prototype, "ntpDefaults must have Object.prototype");
assert.ok(!Object.prototype.hasOwnProperty.call(opts, "__proto__"), "no __proto__ own property on options");
assert.ok(
!Object.prototype.hasOwnProperty.call(opts.ntpDefaults, "__proto__"),
"no __proto__ own property on ntpDefaults"
);

console.log("ok - cjs integration test passed");
36 changes: 36 additions & 0 deletions test/integration/deno-esm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Deno ESM integration test — verifies the ESM dist loads natively under Deno.
// Run: deno run --allow-read test/integration/deno-esm.ts

import { NtpTimeSync } from "../../dist/esm/index.js";

// Instantiate with nested options to exercise recursiveResolveOptions → isPlainObject
new NtpTimeSync({
sampleCount: 4,
ntpDefaults: {
minPoll: 4,
maxPoll: 10,
},
});

// Verify singleton
const a = NtpTimeSync.getInstance();
const b = NtpTimeSync.getInstance();
if (a !== b) {
throw new Error("Singleton check failed: expected same instance");
}

// Verify no __proto__ pollution in resolved options
const opts = (a as unknown as { options: { ntpDefaults: Record<string, unknown> } }).options;
if (Object.getPrototypeOf(opts.ntpDefaults) !== Object.prototype) {
throw new Error("ntpDefaults prototype is not Object.prototype");
}
if (Object.prototype.hasOwnProperty.call(opts, "__proto__")) {
throw new Error("options has unexpected __proto__ own property");
}
if (Object.prototype.hasOwnProperty.call(opts.ntpDefaults, "__proto__")) {
throw new Error("ntpDefaults has unexpected __proto__ own property");
}

console.log("✓ NtpTimeSync imported (ESM) and instantiated");
console.log("✓ Nested options merged without __proto__ issues");
console.log("✓ Singleton pattern works");
18 changes: 15 additions & 3 deletions test/integration/deno.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// Deno integration test — verifies ntp-time-sync works without --unstable-unsafe-proto
// Run: deno run test/integration/deno.ts
// Deno integration test — verifies the CJS dist loads under Deno via createRequire.
// Run: deno run --allow-read test/integration/deno.ts

import { createRequire } from "node:module";

const require = createRequire(import.meta.url);
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { NtpTimeSync } = require("../../dist/index.js");
const { NtpTimeSync } = require("../../dist/cjs/index.js");

// Instantiate with nested options to exercise recursiveResolveOptions → isPlainObject
new NtpTimeSync({
Expand All @@ -23,6 +23,18 @@ if (a !== b) {
throw new Error("Singleton check failed: expected same instance");
}

// Verify no __proto__ pollution in resolved options
const opts = a.options;
if (Object.getPrototypeOf(opts.ntpDefaults) !== Object.prototype) {
throw new Error("ntpDefaults prototype is not Object.prototype");
}
if (Object.prototype.hasOwnProperty.call(opts, "__proto__")) {
throw new Error("options has unexpected __proto__ own property");
}
if (Object.prototype.hasOwnProperty.call(opts.ntpDefaults, "__proto__")) {
throw new Error("ntpDefaults has unexpected __proto__ own property");
}

console.log("✓ NtpTimeSync imported and instantiated");
console.log("✓ Nested options merged without __proto__ issues");
console.log("✓ Singleton pattern works");
42 changes: 42 additions & 0 deletions test/integration/esm.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// ESM integration test — verifies the dual build's ESM output works for `import` consumers.
// Run: node test/integration/esm.mjs

import assert from "node:assert/strict";

import { NtpTimeSync, NtpTimeSyncDefaultOptions } from "../../dist/esm/index.js";

assert.equal(typeof NtpTimeSync, "function", "NtpTimeSync should be exported as a class");
assert.ok(NtpTimeSyncDefaultOptions && typeof NtpTimeSyncDefaultOptions === "object", "defaults should be exported");

// Instantiate with nested options to exercise recursiveResolveOptions → isPlainObject
const instance = new NtpTimeSync({
sampleCount: 4,
ntpDefaults: {
minPoll: 4,
maxPoll: 10,
},
});
assert.ok(instance instanceof NtpTimeSync, "constructor should return an NtpTimeSync instance");

// Verify singleton
const a = NtpTimeSync.getInstance();
const b = NtpTimeSync.getInstance();
assert.equal(a, b, "getInstance must return the same singleton");
assert.ok(a instanceof NtpTimeSync, "singleton must be an NtpTimeSync instance");

// Verify API surface (shape only — no network calls)
assert.equal(typeof a.getTime, "function", "getTime must be a method");
assert.equal(typeof a.now, "function", "now must be a method");
assert.equal(typeof a.getNetworkTime, "function", "getNetworkTime must be a method");

// Verify no __proto__ pollution in resolved options
const opts = /** @type {any} */ (a).options;
assert.ok(opts, "instance.options must be defined");
assert.equal(Object.getPrototypeOf(opts.ntpDefaults), Object.prototype, "ntpDefaults must have Object.prototype");
assert.ok(!Object.prototype.hasOwnProperty.call(opts, "__proto__"), "no __proto__ own property on options");
assert.ok(
!Object.prototype.hasOwnProperty.call(opts.ntpDefaults, "__proto__"),
"no __proto__ own property on ntpDefaults"
);

console.log("ok - esm integration test passed");
20 changes: 20 additions & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"rootDir": "./src",
"allowJs": true,
"lib": [
"es2019"
],
"target": "es2019",
"types": [
"node"
],
"noEmitOnError": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"declaration": true
},
"include": [
"./src/**/*"
]
}
8 changes: 8 additions & 0 deletions tsconfig.cjs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "bundler",
"outDir": "./dist/cjs"
}
}
8 changes: 8 additions & 0 deletions tsconfig.esm.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "es2022",
"moduleResolution": "bundler",
"outDir": "./dist/esm"
}
}
24 changes: 5 additions & 19 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,22 +1,8 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"allowJs": true,
"lib": [
"es2019"
],
"module": "commonjs",
"target": "es2019",
"types": [
"node"
],
"noEmitOnError": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"declaration": true
},
"include": [
"./src/**/*"
]
"module": "es2022",
"moduleResolution": "bundler",
"outDir": "./dist/esm"
}
}