From 752a8f01ae36633d236b1f01f8e502c5912d11dd Mon Sep 17 00:00:00 2001 From: Danilo Alonso Date: Fri, 26 Jun 2026 14:06:42 -0400 Subject: [PATCH 1/7] refactor: convert to native ESM with vitest + oxc toolchain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert @hapi/pinpoint from CommonJS to native ESM and replace the @hapi/lab + @hapi/code toolchain with vitest + @vitest/coverage-v8 and oxlint + oxfmt (via @hapi/oxc-plugin). - "type": "module"; single "." export with a co-located lib/index.d.ts; ship API.md + README.md in the published package - drop the internals namespace - engines.node >= 22; CI on the min-node-22-hapi-21 reusable workflow Preserve the path contract: under ESM, V8's CallSite.getFileName() returns a file:// URL, so location().filename is normalized back to a filesystem path with fileURLToPath (matching the CommonJS output). ESM-only — CommonJS require('@hapi/pinpoint') no longer resolves. --- .github/workflows/ci-module.yml | 14 +++++----- .gitignore | 2 +- API.md | 2 +- lib/index.d.ts | 21 +++++--------- lib/index.js | 20 +++++++------- oxfmt.config.ts | 8 ++++++ oxlint.config.ts | 11 ++++++++ package.json | 49 ++++++++++++++++++++++----------- test/index.js | 45 +++++++++++++++--------------- test/index.ts | 18 ------------ test/typings.ts | 26 +++++++++++++++++ tsconfig.json | 25 +++++++++++++++++ vitest.config.ts | 23 ++++++++++++++++ 13 files changed, 174 insertions(+), 90 deletions(-) create mode 100644 oxfmt.config.ts create mode 100644 oxlint.config.ts delete mode 100755 test/index.ts create mode 100755 test/typings.ts create mode 100644 tsconfig.json create mode 100644 vitest.config.ts diff --git a/.github/workflows/ci-module.yml b/.github/workflows/ci-module.yml index 9dc4315..4b7ee12 100644 --- a/.github/workflows/ci-module.yml +++ b/.github/workflows/ci-module.yml @@ -1,12 +1,12 @@ name: ci on: - push: - branches: - - master - pull_request: - workflow_dispatch: + push: + branches: + - master + pull_request: + workflow_dispatch: jobs: - test: - uses: hapijs/.github/.github/workflows/ci-module.yml@master + test: + uses: hapijs/.github/.github/workflows/ci-module.yml@min-node-22-hapi-21 diff --git a/.gitignore b/.gitignore index 8f679c9..af5a347 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ **/node_modules **/package-lock.json -coverage.* +coverage/ **/.DS_Store **/._* diff --git a/API.md b/API.md index 8f875a7..6bbf3e8 100755 --- a/API.md +++ b/API.md @@ -1,7 +1,7 @@ - ### `location(depth)` Returns the filename and line number of the caller in the call stack where: + - `depth` - the distance from the `location()` function in the call stack. Defaults to `1` (caller). Returns an object with the `filename` and `line` number. diff --git a/lib/index.d.ts b/lib/index.d.ts index 38fadaa..bdae2e9 100755 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -1,24 +1,17 @@ /** -Returns the filename and line number of the caller in the call stack - -@param depth - The distance from the location function in the call stack. Defaults to 1 (caller). - -@return an object with the filename and line number. -*/ + * Returns the filename and line number of the caller in the call stack + * + * @param depth - The distance from the location function in the call stack. Defaults to 1 (caller). + * @returns An object with the filename and line number. + */ export function location(depth?: number): location.Location; declare namespace location { - interface Location { - - /** - The fully qualified filename. - */ + /** The fully qualified filename. */ readonly filename: string; - /** - The file line number. - */ + /** The file line number. */ readonly line: number; } } diff --git a/lib/index.js b/lib/index.js index 4820550..514cf32 100755 --- a/lib/index.js +++ b/lib/index.js @@ -1,21 +1,21 @@ -'use strict'; - -const internals = {}; - - -exports.location = function (depth = 0) { +import { fileURLToPath } from 'url'; +export function location(depth = 0) { const orig = Error.prepareStackTrace; Error.prepareStackTrace = (ignore, stack) => stack; const capture = {}; - Error.captureStackTrace(capture, this); + Error.captureStackTrace(capture); const line = capture.stack[depth + 1]; Error.prepareStackTrace = orig; + // Under ESM, CallSite.getFileName() returns a file:// URL; normalize back to a + // filesystem path so the returned filename matches the CJS-era contract. + const filename = line.getFileName(); + return { - filename: line.getFileName(), - line: line.getLineNumber() + filename: filename && filename.startsWith('file://') ? fileURLToPath(filename) : filename, + line: line.getLineNumber(), }; -}; +} diff --git a/oxfmt.config.ts b/oxfmt.config.ts new file mode 100644 index 0000000..12e357b --- /dev/null +++ b/oxfmt.config.ts @@ -0,0 +1,8 @@ +import DefaultOxfmtConfig from '@hapi/oxc-plugin/oxfmt'; +import { defineConfig } from 'oxfmt'; + +import type { OxfmtConfig } from 'oxfmt'; + +export default defineConfig({ + ...DefaultOxfmtConfig, +}) as OxfmtConfig; diff --git a/oxlint.config.ts b/oxlint.config.ts new file mode 100644 index 0000000..fd4548e --- /dev/null +++ b/oxlint.config.ts @@ -0,0 +1,11 @@ +import HapiRecommended from '@hapi/oxc-plugin/oxlint'; +import { defineConfig } from 'oxlint'; + +import type { OxlintConfig } from 'oxlint'; + +export default defineConfig({ + extends: [HapiRecommended], + env: { + ...HapiRecommended.env, + }, +}) as OxlintConfig; diff --git a/package.json b/package.json index 3c9d7be..29ef903 100755 --- a/package.json +++ b/package.json @@ -1,26 +1,43 @@ { "name": "@hapi/pinpoint", - "description": "Return the filename and line number of the calling function", "version": "2.0.1", - "repository": "git://github.com/hapijs/pinpoint", - "main": "lib/index.js", - "types": "lib/index.d.ts", - "files": [ - "lib" - ], + "description": "Return the filename and line number of the calling function", "keywords": [ "utilities" ], - "dependencies": {}, - "devDependencies": { - "@hapi/code": "^9.0.3", - "@hapi/lab": "^25.1.2", - "@types/node": "^14.18.36", - "typescript": "4.0.x" + "license": "BSD-3-Clause", + "repository": "git://github.com/hapijs/pinpoint", + "files": [ + "lib", + "API.md", + "README.md" + ], + "type": "module", + "types": "lib/index.d.ts", + "exports": { + ".": { + "types": "./lib/index.d.ts", + "default": "./lib/index.js" + } }, "scripts": { - "test": "lab -a @hapi/code -t 100 -L -Y", - "test-cov-html": "lab -a @hapi/code -t 100 -L -r html -o coverage.html" + "test": "vitest run --coverage", + "typecheck": "tsc --noEmit", + "lint": "oxlint", + "lint:fix": "oxlint --fix", + "fmt": "oxfmt --check", + "fmt:fix": "oxfmt", + "check": "npm run lint && npm run fmt && npm run typecheck && npm test" + }, + "devDependencies": { + "@hapi/oxc-plugin": "^1.0.1", + "@vitest/coverage-v8": "^4.1.9", + "oxfmt": "^0.55.0", + "oxlint": "^1.70.0", + "typescript": "^6.0.3", + "vitest": "^4.1.9" }, - "license": "BSD-3-Clause" + "engines": { + "node": ">=22" + } } diff --git a/test/index.js b/test/index.js index 009d2d8..ac0aeb1 100755 --- a/test/index.js +++ b/test/index.js @@ -1,34 +1,33 @@ -'use strict'; - -const Path = require('path'); - -const Code = require('@hapi/code'); -const Lab = require('@hapi/lab'); -const Pinpoint = require('..'); - - -const internals = {}; - - -const { describe, it } = exports.lab = Lab.script(); -const expect = Code.expect; +import { describe, expect, it } from 'vitest'; +import * as Pinpoint from '../lib/index.js'; describe('Pinpoint', () => { - describe('location()', () => { + it('returns the filename and line of the calling frame', () => { + const first = Pinpoint.location(); + const second = Pinpoint.location(); // exactly one source line below `first` + + expect(first.filename).toContain('index.js'); + expect(first.filename.startsWith('file://')).toBe(false); // normalized to a path, not a URL + expect(typeof first.line).toBe('number'); + + // Adjacent calls report consecutive lines. Asserting the delta (not an + // absolute line) is robust to any constant line offset a test runner's + // module transform introduces. + expect(second.line - first.line).toBe(1); + }); - it('returns current location', () => { + it('walks up the stack with depth', () => { + const inner = () => Pinpoint.location(1); // depth 1 → inner's caller - expect(Pinpoint.location()).to.equal({ - filename: Path.join(__dirname, 'index.js'), - line: 23 - }); - }); + const mark = Pinpoint.location(); // depth 0 → this line + const up = inner(); // inner() is one source line below `mark` - it('returns location or upper caller', () => { + expect(up.filename).toContain('index.js'); - expect(Pinpoint.location(1).filename).to.contain(Path.join('@hapi', 'lab')); + // depth 1 resolves to where inner() was invoked — one line below `mark`. + expect(up.line - mark.line).toBe(1); }); }); }); diff --git a/test/index.ts b/test/index.ts deleted file mode 100755 index fbf93e2..0000000 --- a/test/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as Pinpoint from '..'; -import * as Lab from '@hapi/lab'; - - -const { expect } = Lab.types; - - -// location() - -Pinpoint.location(); -Pinpoint.location(1); - -expect.type(Pinpoint.location()); -expect.type(Pinpoint.location().filename); -expect.type(Pinpoint.location().line); - -expect.error(Pinpoint.location(1, 2)); -expect.error(Pinpoint.location('1')); diff --git a/test/typings.ts b/test/typings.ts new file mode 100755 index 0000000..cf9e22f --- /dev/null +++ b/test/typings.ts @@ -0,0 +1,26 @@ +import { describe, expectTypeOf, it } from 'vitest'; + +import * as Pinpoint from '../lib/index.js'; + +describe('typings', () => { + describe('location', () => { + it('return type + signature compile', () => { + expectTypeOf(Pinpoint.location).toBeFunction(); + expectTypeOf(Pinpoint.location()).toEqualTypeOf(); + expectTypeOf(Pinpoint.location().filename).toBeString(); + expectTypeOf(Pinpoint.location().line).toBeNumber(); + expectTypeOf(Pinpoint.location(1)).toEqualTypeOf(); + }); + + it('invalid calls rejected', () => { + // The @ts-expect-error directives are the assertion; the calls still run, so + // wrap them — a non-numeric depth throws at runtime (the type errors are what we test). + try { + // @ts-expect-error depth takes at most one argument + Pinpoint.location(1, 2); + // @ts-expect-error depth must be a number + Pinpoint.location('1'); + } catch {} + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..849762f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, + "inlineSources": true, + "isolatedDeclarations": true, + "isolatedModules": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipDefaultLibCheck": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "ESNext" + } +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..df8ecc7 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,23 @@ +import Oxc from '@hapi/oxc-plugin/vitest'; +import { defineConfig } from 'vitest/config'; + +import type { ViteUserConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [Oxc()], + test: { + environment: 'node', + include: ['test/**/*.{js,ts}'], + typecheck: { + enabled: true, + include: ['test/**/*.{js,ts}'], + }, + coverage: { + provider: 'v8', + include: ['lib/**'], + thresholds: { + 100: true, + }, + }, + }, +}) as ViteUserConfig; From 86a86a87b2d30209d84fa607ccde537b27357dbe Mon Sep 17 00:00:00 2001 From: Danilo Alonso Date: Fri, 26 Jun 2026 23:48:57 -0400 Subject: [PATCH 2/7] refactor: git mv source to src/ with .mjs extension Pure rename (no content change) so the move is tracked as a rename; reference fixups follow in the next commit. --- lib/index.d.ts => src/index.d.mts | 0 lib/index.js => src/index.mjs | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename lib/index.d.ts => src/index.d.mts (100%) rename lib/index.js => src/index.mjs (100%) diff --git a/lib/index.d.ts b/src/index.d.mts similarity index 100% rename from lib/index.d.ts rename to src/index.d.mts diff --git a/lib/index.js b/src/index.mjs similarity index 100% rename from lib/index.js rename to src/index.mjs From 4dc447d74d3474d603b0073d6613886f35cb5e60 Mon Sep 17 00:00:00 2001 From: Danilo Alonso Date: Fri, 26 Jun 2026 23:49:00 -0400 Subject: [PATCH 3/7] refactor: repoint exports, imports, and tests to src/*.mjs package.json exports/types -> src/index.mjs + src/index.d.mts; vitest coverage include -> src/**; test imports -> ../src/index.mjs; b64 also updates index.mjs internal re-exports. --- package.json | 8 ++++---- test/index.js | 2 +- test/typings.ts | 2 +- vitest.config.ts | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 29ef903..0214f91 100755 --- a/package.json +++ b/package.json @@ -8,16 +8,16 @@ "license": "BSD-3-Clause", "repository": "git://github.com/hapijs/pinpoint", "files": [ - "lib", + "src", "API.md", "README.md" ], "type": "module", - "types": "lib/index.d.ts", + "types": "src/index.d.mts", "exports": { ".": { - "types": "./lib/index.d.ts", - "default": "./lib/index.js" + "types": "./src/index.d.mts", + "default": "./src/index.mjs" } }, "scripts": { diff --git a/test/index.js b/test/index.js index ac0aeb1..4612bce 100755 --- a/test/index.js +++ b/test/index.js @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import * as Pinpoint from '../lib/index.js'; +import * as Pinpoint from '../src/index.mjs'; describe('Pinpoint', () => { describe('location()', () => { diff --git a/test/typings.ts b/test/typings.ts index cf9e22f..491d9d5 100755 --- a/test/typings.ts +++ b/test/typings.ts @@ -1,6 +1,6 @@ import { describe, expectTypeOf, it } from 'vitest'; -import * as Pinpoint from '../lib/index.js'; +import * as Pinpoint from '../src/index.mjs'; describe('typings', () => { describe('location', () => { diff --git a/vitest.config.ts b/vitest.config.ts index df8ecc7..2420cb5 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -14,7 +14,7 @@ export default defineConfig({ }, coverage: { provider: 'v8', - include: ['lib/**'], + include: ['src/**'], thresholds: { 100: true, }, From 88df51aeca39e1d37d9b97b8cf904a220327ac86 Mon Sep 17 00:00:00 2001 From: Nicolas Morel Date: Wed, 1 Jul 2026 15:21:46 +0200 Subject: [PATCH 4/7] chore: revert rename --- package.json | 6 +++--- src/{index.d.mts => index.d.ts} | 0 src/{index.mjs => index.js} | 0 test/index.js | 2 +- test/typings.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename src/{index.d.mts => index.d.ts} (100%) rename src/{index.mjs => index.js} (100%) diff --git a/package.json b/package.json index 0214f91..e6c409c 100755 --- a/package.json +++ b/package.json @@ -13,11 +13,11 @@ "README.md" ], "type": "module", - "types": "src/index.d.mts", + "types": "src/index.d.ts", "exports": { ".": { - "types": "./src/index.d.mts", - "default": "./src/index.mjs" + "types": "./src/index.d.ts", + "default": "./src/index.js" } }, "scripts": { diff --git a/src/index.d.mts b/src/index.d.ts similarity index 100% rename from src/index.d.mts rename to src/index.d.ts diff --git a/src/index.mjs b/src/index.js similarity index 100% rename from src/index.mjs rename to src/index.js diff --git a/test/index.js b/test/index.js index 4612bce..51f80d5 100755 --- a/test/index.js +++ b/test/index.js @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import * as Pinpoint from '../src/index.mjs'; +import * as Pinpoint from '../src/index.js'; describe('Pinpoint', () => { describe('location()', () => { diff --git a/test/typings.ts b/test/typings.ts index 491d9d5..2b5ce2c 100755 --- a/test/typings.ts +++ b/test/typings.ts @@ -1,6 +1,6 @@ import { describe, expectTypeOf, it } from 'vitest'; -import * as Pinpoint from '../src/index.mjs'; +import * as Pinpoint from '../src/index.js'; describe('typings', () => { describe('location', () => { From 251ab2af37e62234885b75fc6591695fccd18fdc Mon Sep 17 00:00:00 2001 From: Nicolas Morel Date: Wed, 1 Jul 2026 15:22:14 +0200 Subject: [PATCH 5/7] chore: remove implicit README.md --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index e6c409c..e11a893 100755 --- a/package.json +++ b/package.json @@ -9,8 +9,7 @@ "repository": "git://github.com/hapijs/pinpoint", "files": [ "src", - "API.md", - "README.md" + "API.md" ], "type": "module", "types": "src/index.d.ts", From b26f3ae7872eb2bd7b8150eb839f8ce598f2d817 Mon Sep 17 00:00:00 2001 From: Nicolas Morel Date: Wed, 1 Jul 2026 15:24:23 +0200 Subject: [PATCH 6/7] chore: bump linting --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e11a893..607b30b 100755 --- a/package.json +++ b/package.json @@ -31,8 +31,8 @@ "devDependencies": { "@hapi/oxc-plugin": "^1.0.1", "@vitest/coverage-v8": "^4.1.9", - "oxfmt": "^0.55.0", - "oxlint": "^1.70.0", + "oxfmt": "^0.57.0", + "oxlint": "^1.72.0", "typescript": "^6.0.3", "vitest": "^4.1.9" }, From a35c6c202a4c734319927c95bb6b90937ee23587 Mon Sep 17 00:00:00 2001 From: Nicolas Morel Date: Wed, 1 Jul 2026 15:47:41 +0200 Subject: [PATCH 7/7] chore: remove excess comments and fix the doc --- API.md | 2 +- test/index.js | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/API.md b/API.md index 6bbf3e8..709b8eb 100755 --- a/API.md +++ b/API.md @@ -2,6 +2,6 @@ Returns the filename and line number of the caller in the call stack where: -- `depth` - the distance from the `location()` function in the call stack. Defaults to `1` (caller). +- `depth` - the distance from the `location()` function in the call stack. Defaults to `0` (immediate caller); each increment walks one frame further up. Returns an object with the `filename` and `line` number. diff --git a/test/index.js b/test/index.js index 51f80d5..24d6777 100755 --- a/test/index.js +++ b/test/index.js @@ -11,10 +11,6 @@ describe('Pinpoint', () => { expect(first.filename).toContain('index.js'); expect(first.filename.startsWith('file://')).toBe(false); // normalized to a path, not a URL expect(typeof first.line).toBe('number'); - - // Adjacent calls report consecutive lines. Asserting the delta (not an - // absolute line) is robust to any constant line offset a test runner's - // module transform introduces. expect(second.line - first.line).toBe(1); }); @@ -25,8 +21,6 @@ describe('Pinpoint', () => { const up = inner(); // inner() is one source line below `mark` expect(up.filename).toContain('index.js'); - - // depth 1 resolves to where inner() was invoked — one line below `mark`. expect(up.line - mark.line).toBe(1); }); });