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..709b8eb 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). + +- `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/lib/index.d.ts b/lib/index.d.ts deleted file mode 100755 index 38fadaa..0000000 --- a/lib/index.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** -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. -*/ -export function location(depth?: number): location.Location; - -declare namespace location { - - interface Location { - - /** - The fully qualified filename. - */ - readonly filename: string; - - /** - The file line number. - */ - readonly line: number; - } -} diff --git a/lib/index.js b/lib/index.js deleted file mode 100755 index 4820550..0000000 --- a/lib/index.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -const internals = {}; - - -exports.location = function (depth = 0) { - - const orig = Error.prepareStackTrace; - Error.prepareStackTrace = (ignore, stack) => stack; - - const capture = {}; - Error.captureStackTrace(capture, this); - const line = capture.stack[depth + 1]; - - Error.prepareStackTrace = orig; - - return { - filename: line.getFileName(), - 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..607b30b 100755 --- a/package.json +++ b/package.json @@ -1,26 +1,42 @@ { "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": [ + "src", + "API.md" + ], + "type": "module", + "types": "src/index.d.ts", + "exports": { + ".": { + "types": "./src/index.d.ts", + "default": "./src/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.57.0", + "oxlint": "^1.72.0", + "typescript": "^6.0.3", + "vitest": "^4.1.9" }, - "license": "BSD-3-Clause" + "engines": { + "node": ">=22" + } } diff --git a/src/index.d.ts b/src/index.d.ts new file mode 100755 index 0000000..bdae2e9 --- /dev/null +++ b/src/index.d.ts @@ -0,0 +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). + * @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. */ + readonly filename: string; + + /** The file line number. */ + readonly line: number; + } +} diff --git a/src/index.js b/src/index.js new file mode 100755 index 0000000..514cf32 --- /dev/null +++ b/src/index.js @@ -0,0 +1,21 @@ +import { fileURLToPath } from 'url'; + +export function location(depth = 0) { + const orig = Error.prepareStackTrace; + Error.prepareStackTrace = (ignore, stack) => stack; + + const capture = {}; + 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: filename && filename.startsWith('file://') ? fileURLToPath(filename) : filename, + line: line.getLineNumber(), + }; +} diff --git a/test/index.js b/test/index.js index 009d2d8..24d6777 100755 --- a/test/index.js +++ b/test/index.js @@ -1,34 +1,27 @@ -'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 '../src/index.js'; describe('Pinpoint', () => { - describe('location()', () => { - - it('returns current location', () => { - - expect(Pinpoint.location()).to.equal({ - filename: Path.join(__dirname, 'index.js'), - line: 23 - }); + 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'); + expect(second.line - first.line).toBe(1); }); - it('returns location or upper caller', () => { + it('walks up the stack with depth', () => { + const inner = () => Pinpoint.location(1); // depth 1 → inner's caller + + const mark = Pinpoint.location(); // depth 0 → this line + const up = inner(); // inner() is one source line below `mark` - expect(Pinpoint.location(1).filename).to.contain(Path.join('@hapi', 'lab')); + expect(up.filename).toContain('index.js'); + 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..2b5ce2c --- /dev/null +++ b/test/typings.ts @@ -0,0 +1,26 @@ +import { describe, expectTypeOf, it } from 'vitest'; + +import * as Pinpoint from '../src/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..2420cb5 --- /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: ['src/**'], + thresholds: { + 100: true, + }, + }, + }, +}) as ViteUserConfig;