diff --git a/.npmrc b/.npmrc index 9951b11..3757b30 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1,2 @@ -package-lock=false ignore-scripts=true +package-lock=false diff --git a/lib/index.js b/lib/index.js index bc4853a..5171f3d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,47 +1,42 @@ /** - * @typedef {import('mdast').Root} Root - * @typedef {import('mdast').Content} Content * @typedef {import('mdast').Definition} Definition + * @typedef {import('mdast').Nodes} Nodes */ /** - * @typedef {Root | Content} Node - * * @callback GetDefinition * Get a definition by identifier. * @param {string | null | undefined} [identifier] - * Identifier of definition. - * @returns {Definition | null} + * Identifier of definition (optional). + * @returns {Definition | undefined} * Definition corresponding to `identifier` or `null`. */ import {visit} from 'unist-util-visit' -const own = {}.hasOwnProperty - /** * Find definitions in `tree`. * * Uses CommonMark precedence, which means that earlier definitions are * preferred over duplicate later definitions. * - * @param {Node} tree + * @param {Nodes} tree * Tree to check. * @returns {GetDefinition} * Getter. */ export function definitions(tree) { - /** @type {Record} */ - const cache = Object.create(null) + /** @type {Map} */ + const cache = new Map() if (!tree || !tree.type) { throw new Error('mdast-util-definitions expected node') } - visit(tree, 'definition', (definition) => { + visit(tree, 'definition', function (definition) { const id = clean(definition.identifier) - if (id && !own.call(cache, id)) { - cache[id] = definition + if (id && !cache.get(id)) { + cache.set(id, definition) } }) @@ -50,8 +45,7 @@ export function definitions(tree) { /** @type {GetDefinition} */ function definition(identifier) { const id = clean(identifier) - // To do: next major: return `undefined` when not found. - return id && own.call(cache, id) ? cache[id] : null + return cache.get(id) } } diff --git a/package.json b/package.json index cc1ae71..04c8cf4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mdast-util-definitions", - "version": "5.1.2", + "version": "6.0.0", "description": "mdast utility to find definition nodes in a tree", "license": "MIT", "keywords": [ @@ -28,56 +28,56 @@ ], "sideEffects": false, "type": "module", - "main": "index.js", - "types": "index.d.ts", + "exports": "./index.js", "files": [ "lib/", "index.d.ts", "index.js" ], "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "unist-util-visit": "^4.0.0" + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" }, "devDependencies": { - "@types/node": "^18.0.0", - "c8": "^7.0.0", + "@types/node": "^20.0.0", + "c8": "^8.0.0", "mdast-util-from-markdown": "^1.0.0", "prettier": "^2.0.0", "remark-cli": "^11.0.0", "remark-preset-wooorm": "^9.0.0", "type-coverage": "^2.0.0", - "typescript": "^4.0.0", - "xo": "^0.53.0" + "typescript": "^5.0.0", + "xo": "^0.54.0" }, "scripts": { "prepack": "npm run build && npm run format", "build": "tsc --build --clean && tsc --build && type-coverage", "format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", "test-api": "node --conditions development test.js", - "test-coverage": "c8 --check-coverage --100 --reporter lcov npm run test-api", + "test-coverage": "c8 --100 --reporter lcov npm run test-api", "test": "npm run build && npm run format && npm run test-coverage" }, "prettier": { - "tabWidth": 2, - "useTabs": false, - "singleQuote": true, "bracketSpacing": false, "semi": false, - "trailingComma": "none" - }, - "xo": { - "prettier": true + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "none", + "useTabs": false }, "remarkConfig": { "plugins": [ - "preset-wooorm" + "remark-preset-wooorm" ] }, "typeCoverage": { "atLeast": 100, "detail": true, + "ignoreCatch": true, "strict": true + }, + "xo": { + "prettier": true } } diff --git a/readme.md b/readme.md index 078c402..171a89a 100644 --- a/readme.md +++ b/readme.md @@ -39,7 +39,7 @@ It’s small and protects against prototype pollution. ## Install This package is [ESM only][esm]. -In Node.js (version 14.14+ and 16.0+), install with [npm][]: +In Node.js (version 16+), install with [npm][]: ```sh npm install mdast-util-definitions @@ -48,22 +48,22 @@ npm install mdast-util-definitions In Deno with [`esm.sh`][esmsh]: ```js -import {definitions} from 'https://esm.sh/mdast-util-definitions@5' +import {definitions} from 'https://esm.sh/mdast-util-definitions@6' ``` In browsers with [`esm.sh`][esmsh]: ```html ``` ## Use ```js -import {fromMarkdown} from 'mdast-util-from-markdown' import {definitions} from 'mdast-util-definitions' +import {fromMarkdown} from 'mdast-util-from-markdown' const tree = fromMarkdown('[example]: https://example.com "Example"') @@ -73,7 +73,7 @@ definition('example') // => {type: 'definition', 'title': 'Example', …} definition('foo') -// => null +// => undefined ``` ## API @@ -109,7 +109,7 @@ Get a definition by identifier (TypeScript type). ###### Returns Definition corresponding to `identifier` ([`Definition`][definition]) or -`null`. +`undefined`. ## Types @@ -118,10 +118,13 @@ It exports the additional type [`GetDefinition`][api-getdefinition]. ## Compatibility -Projects maintained by the unified collective are compatible with all maintained +Projects maintained by the unified collective are compatible with maintained versions of Node.js. -As of now, that is Node.js 14.14+ and 16.0+. -Our projects sometimes work with older versions, but this is not guaranteed. + +When we cut a new major release, we drop support for unmaintained versions of +Node. +This means we try to keep the current release line, +`mdast-util-definitions@^6`, compatible with Node.js 16. ## Security @@ -162,9 +165,9 @@ abide by its terms. [downloads]: https://www.npmjs.com/package/mdast-util-definitions -[size-badge]: https://img.shields.io/bundlephobia/minzip/mdast-util-definitions.svg +[size-badge]: https://img.shields.io/badge/dynamic/json?label=minzipped%20size&query=$.size.compressedSize&url=https://deno.bundlejs.com/?q=mdast-util-definitions -[size]: https://bundlephobia.com/result?p=mdast-util-definitions +[size]: https://bundlejs.com/?q=mdast-util-definitions [sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg diff --git a/test.js b/test.js index ec76bc2..4291d3e 100644 --- a/test.js +++ b/test.js @@ -1,98 +1,115 @@ +/** + * @typedef {import('mdast').Root} Root + */ + import assert from 'node:assert/strict' import test from 'node:test' +import {definitions} from 'mdast-util-definitions' import {fromMarkdown} from 'mdast-util-from-markdown' -import {definitions} from './index.js' -import * as mod from './index.js' -test('definitions', () => { - assert.deepEqual( - Object.keys(mod).sort(), - ['definitions'], - 'should expose the public api' - ) +test('definitions', async function (t) { + await t.test('should expose the public api', async function () { + assert.deepEqual( + Object.keys(await import('mdast-util-definitions')).sort(), + ['definitions'] + ) + }) - assert.throws( - () => { - // @ts-expect-error runtime + await t.test('should fail without node', async function () { + assert.throws(function () { + // @ts-expect-error: check that an error is thrown at runtime. definitions() - }, - /mdast-util-definitions expected node/, - 'should fail without node' - ) + }, /mdast-util-definitions expected node/) + }) - assert.deepEqual( - definitions(fromMarkdown('[example]: https://example.com "Example"'))( - 'example' - ), - { - type: 'definition', - identifier: 'example', - label: 'example', - title: 'Example', - url: 'https://example.com', - position: { - start: {line: 1, column: 1, offset: 0}, - end: {line: 1, column: 41, offset: 40} + await t.test('should return a definition', async function () { + assert.deepEqual( + definitions(from('[example]: https://example.com "Example"'))('example'), + { + type: 'definition', + identifier: 'example', + label: 'example', + title: 'Example', + url: 'https://example.com', + position: { + start: {line: 1, column: 1, offset: 0}, + end: {line: 1, column: 41, offset: 40} + } } - }, - 'should return a definition' - ) + ) + }) - assert.equal( - definitions(fromMarkdown('[example]: https://example.com "Example"'))( - 'foo' - ), - null, - 'should return null when not found' - ) + await t.test('should return `undefined` when not found', async function () { + assert.equal( + definitions(from('[example]: https://example.com "Example"'))('foo'), + undefined + ) + }) - assert.deepEqual( - definitions(fromMarkdown('[__proto__]: https://proto.com "Proto"'))( - '__proto__' - ), - { - type: 'definition', - identifier: '__proto__', - label: '__proto__', - title: 'Proto', - url: 'https://proto.com', - position: { - start: {line: 1, column: 1, offset: 0}, - end: {line: 1, column: 39, offset: 38} + await t.test('should work on weird identifiers', async function () { + assert.deepEqual( + definitions(from('[__proto__]: https://proto.com "Proto"'))('__proto__'), + { + type: 'definition', + identifier: '__proto__', + label: '__proto__', + title: 'Proto', + url: 'https://proto.com', + position: { + start: {line: 1, column: 1, offset: 0}, + end: {line: 1, column: 39, offset: 38} + } } - }, - 'should work on weird identifiers' - ) + ) + }) - /* eslint-disable no-use-extend-native/no-use-extend-native */ - // @ts-expect-error: yes. - // type-coverage:ignore-next-line - assert.equal({}.type, undefined, 'should not polute the prototype') - /* eslint-enable no-use-extend-native/no-use-extend-native */ + await t.test('should not polute the prototype', async function () { + /* eslint-disable no-use-extend-native/no-use-extend-native */ + // @ts-expect-error: yes. + // type-coverage:ignore-next-line + assert.equal({}.type, undefined) + /* eslint-enable no-use-extend-native/no-use-extend-native */ + }) - assert.deepEqual( - definitions(fromMarkdown('[__proto__]: https://proto.com "Proto"'))( - 'toString' - ), - null, - 'should work on weird identifiers when not found' + await t.test( + 'should work on weird identifiers when not found', + async function () { + assert.deepEqual( + definitions(from('[__proto__]: https://proto.com "Proto"'))('toString'), + undefined + ) + } ) - const example = definitions( - fromMarkdown('[example]: https://one.com\n[example]: https://two.com') - )('example') + await t.test( + 'should prefer the first of duplicate definitions', + async function () { + const example = definitions( + from('[example]: https://one.com\n[example]: https://two.com') + )('example') - assert.deepEqual( - example && example.url, - 'https://one.com', - 'should prefer the first of duplicate definitions' + assert.deepEqual(example && example.url, 'https://one.com') + } ) - assert.deepEqual( - definitions( - fromMarkdown('[example]: https://one.com\n[example]: https://two.com') - )(''), - null, - 'should not return something for a missing identifier' + await t.test( + 'should not return something for a missing identifier', + async function () { + assert.deepEqual( + definitions( + from('[example]: https://one.com\n[example]: https://two.com') + )(''), + undefined + ) + } ) }) + +/** + * @param {string} value + * @returns {Root} + */ +function from(value) { + // @ts-expect-error: To do: remove cast when `from-markdown` is released. + return fromMarkdown(value) +} diff --git a/tsconfig.json b/tsconfig.json index ebe8889..82cc749 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,17 +1,15 @@ { - "include": ["**/*.js"], - "exclude": ["coverage/", "node_modules/"], "compilerOptions": { "checkJs": true, + "customConditions": ["development"], "declaration": true, "emitDeclarationOnly": true, "exactOptionalPropertyTypes": true, - "forceConsistentCasingInFileNames": true, - "lib": ["es2020"], + "lib": ["es2022"], "module": "node16", - "newLine": "lf", - "skipLibCheck": true, "strict": true, - "target": "es2020" - } + "target": "es2022" + }, + "exclude": ["coverage/", "node_modules/"], + "include": ["**/*.js"] }