diff --git a/lib/find-license.js b/lib/find-license.js index a34c141..eb34de0 100644 --- a/lib/find-license.js +++ b/lib/find-license.js @@ -1,10 +1,12 @@ -import {promises as fs} from 'node:fs' +import fs from 'node:fs/promises' const licenseRegexp = /^licen[cs]e(?=$|\.)/i /** * @param {string} base - * @returns {Promise} + * Folder. + * @returns {Promise} + * Basename of license file. */ export async function findLicense(base) { const files = await fs.readdir(base) diff --git a/lib/find-nearest-package.js b/lib/find-nearest-package.js index 2728409..14f06ef 100644 --- a/lib/find-nearest-package.js +++ b/lib/find-nearest-package.js @@ -1,25 +1,49 @@ +/** + * @typedef {import('vfile').VFile} VFile + */ + +/** + * @typedef Info + * Info. + * @property {string} folder + * Path to folder with `package.json`. + * @property {string | undefined} license + * SPDX identifier. + * @property {string | undefined} name + * Name of author. + * @property {string | undefined} url + * URL for author. + */ + +import fs from 'node:fs/promises' +import path from 'node:path' import parse from 'parse-author' -import {read} from 'to-vfile' import {findUp} from 'vfile-find-up' /** - * @param {string} base - * @returns {Promise<{license: string|undefined, name: string|undefined, url: string|undefined}>} + * @param {VFile} from + * File to resolve from. + * @returns {Promise} + * Info. */ -export async function findNearestPackage(base) { +export async function findNearestPackage(from) { + /* c8 ignore next -- else is for stdin, typically not used. */ + const base = from.dirname ? path.resolve(from.cwd, from.dirname) : from.cwd const file = await findUp('package.json', base) if (file) { - await read(file) + file.value = await fs.readFile(file.path) /** @type {import('type-fest').PackageJson} */ const json = JSON.parse(String(file)) const author = typeof json.author === 'string' ? parse(json.author) : json.author || {} - return {license: json.license, name: author.name, url: author.url} + return { + /* c8 ignore next -- always defined. */ + folder: file.dirname ? path.resolve(file.cwd, file.dirname) : file.cwd, + license: json.license, + name: author.name, + url: author.url + } } - - // V8 bug on Node 12. - /* c8 ignore next 2 */ - return {license: undefined, name: undefined, url: undefined} } diff --git a/lib/index.js b/lib/index.js index cede4f3..d08f1cf 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,102 +1,89 @@ /** - * @typedef {import('mdast').Root} Root - * @typedef {import('mdast').Paragraph} Paragraph * @typedef {import('mdast').Link} Link + * @typedef {import('mdast').Paragraph} Paragraph * @typedef {import('mdast').PhrasingContent} PhrasingContent + * @typedef {import('mdast').Root} Root * + * @typedef {import('vfile').VFile} VFile + */ + +/** * @typedef Options * Configuration (optional). - * @property {string} [name] - * License holder. - * Detected from the `package.json` closest to the file supporting both - * `object` and `string` format of `author`. - * *Throws when neither given nor detected.* - * @property {string} [license] - * SPDX identifier - * Detected from the `license` field in the `package.json` in the current - * working directory. - * Deprecated license objects are not supported. - * *Throws when neither given nor detected.* - * @property {string} [file] - * File name of license file - * Detected from the files in the directory of the `package.json` if there is + * @property {boolean | null | undefined} [ignoreFinalDefinitions=true] + * Ignore definitions at the end of the license section (default: `true`). + * @property {string | null | undefined} [file] + * Path to license file (optional); + * detected from the files in the directory of the `package.json` if there is * one, or the current working directory, in which case the first file - * matching `/^licen[cs]e(?=$|\.)/i` is used. - * If there is no given or found license file, but `options.license` is a + * matching `/^licen[cs]e(?=$|\.)/i` is used; + * if there is no given or found license file, but `options.license` is a * known SPDX identifier, the URL to the license on `spdx.org` is used. - * @property {string} [url] - * URL to license holder - * Detected from the `package.json` in the current working directory, - * supporting both `object` and `string` format of `author`. - * `http://` is prepended if `url` starts without HTTP or HTTPS protocol. - * @property {boolean} [ignoreFinalDefinitions=true] - * Ignore final definitions otherwise in the section. - * @property {string|RegExp} [heading] - * Heading to look for. - * Default: `/^licen[cs]e$/i`. + * @property {RegExp | string | null | undefined} [heading] + * Heading to look for (default: `/^licen[cs]e$/i`). + * @property {string | null | undefined} [license] + * SPDX identifier (optional, example: `'mit'`); + * detected from the `license` field in the `package.json` in the current + * working directory; + * throws when neither given nor detected. + * @property {string | null | undefined} [name] + * License holder (optional); + * detected from the `package.json` closest to the file supporting both + * `object` and `string` format of `author`; + * throws when neither given nor detected. + * @property {string | null | undefined} [url] + * URL to license holder (optional); + * detected from the `package.json` in the current working directory. */ -import path from 'node:path' -import spdx from 'spdx-license-list' -import {findUp} from 'vfile-find-up' import {headingRange} from 'mdast-util-heading-range' +import spdx from 'spdx-license-list' import {findNearestPackage} from './find-nearest-package.js' import {findLicense} from './find-license.js' -const licenseHeadingRegexp = /^licen[cs]e$/i const http = 'http://' const https = 'https://' +const licenseHeadingRegexp = /^licen[cs]e$/i + +/** @type {Readonly} */ +const emptyOptions = {} /** - * Plugin to generate a license section. + * Generate a license section. * - * @type {import('unified').Plugin<[Options], Root>} + * @param {Readonly | null | undefined} [options] + * Configuration (optional). + * @returns + * Transform. */ -export default function remarkLicense(options = {}) { - const ignoreFinalDefinitions = - options.ignoreFinalDefinitions === undefined || - options.ignoreFinalDefinitions === null - ? true - : options.ignoreFinalDefinitions - const test = options.heading || licenseHeadingRegexp - +export default function remarkLicense(options) { + const settings = options || emptyOptions + const ignoreFinalDefinitions = settings.ignoreFinalDefinitions !== false + const test = settings.heading || licenseHeadingRegexp + + /** + * Transform. + * + * @param {Root} tree + * Tree. + * @param {VFile} file + * File. + * @returns {Promise} + * Nothing. + */ return async function (tree, file) { - // Else is for stdin, typically not used. - /* c8 ignore next */ - const base = file.dirname ? path.resolve(file.cwd, file.dirname) : file.cwd - const packageFile = await findUp('package.json', base) - /** @type {string|undefined} */ - let defaultName - /** @type {string|undefined} */ - let defaultUrl - /** @type {string|undefined} */ - let defaultLicense - /** @type {string|undefined} */ - let defaultLicenseFile - - // Skip package loading if we have all info in `options`. - if (!options.url || !options.name || !options.license) { - const result = await findNearestPackage(base) - defaultLicense = result.license - defaultName = result.name - defaultUrl = result.url - } - - if (!options.file) { - defaultLicenseFile = await findLicense( - (packageFile && - packageFile.dirname && - path.resolve(packageFile.cwd, packageFile.dirname)) || - file.cwd - ) - } - - const url = options.url || defaultUrl - const name = options.name || defaultName - const license = options.license || defaultLicense - let licenseFile = options.file || defaultLicenseFile - - /* Ignore the license file itself. */ + const info = + !settings.url || !settings.name || !settings.license || !settings.file + ? await findNearestPackage(file) + : undefined + const url = settings.url || (info ? info.url : undefined) + const name = settings.name || (info ? info.name : undefined) + const license = settings.license || (info ? info.license : undefined) + let licenseFile = + settings.file || + (await findLicense((info ? info.folder : undefined) || file.cwd)) + + // Ignore the license file itself. if (licenseFile && file.path === licenseFile) { return } @@ -125,11 +112,11 @@ export default function remarkLicense(options = {}) { tree, {ignoreFinalDefinitions, test}, function (start, _, end) { - /** @type {PhrasingContent[]} */ + /** @type {Array} */ const children = [] /** @type {Paragraph} */ const node = {type: 'paragraph', children} - /** @type {Paragraph|Link} */ + /** @type {Link | Paragraph} */ let parent if (licenseFile) { diff --git a/package.json b/package.json index e10f25c..e809da5 100644 --- a/package.json +++ b/package.json @@ -46,8 +46,7 @@ "mdast-util-heading-range": "^4.0.0", "parse-author": "^2.0.0", "spdx-license-list": "^6.0.0", - "to-vfile": "^8.0.0", - "unified": "^11.0.0", + "vfile": "^6.0.0", "vfile-find-up": "^7.0.0" }, "devDependencies": { diff --git a/test/index.js b/test/index.js index 6a6db48..43782c0 100644 --- a/test/index.js +++ b/test/index.js @@ -11,6 +11,14 @@ import {remark} from 'remark' import semver from 'semver' import license from '../index.js' +test('remark-license', async function (t) { + await t.test('should expose the public api', async function () { + assert.deepEqual(Object.keys(await import('../index.js')).sort(), [ + 'default' + ]) + }) +}) + test('fixtures', async function (t) { // Prepapre. const root = new URL('fixtures/', import.meta.url) @@ -62,7 +70,6 @@ test('fixtures', async function (t) { try { const file = await remark() - // @ts-expect-error: to do: remove after update. .use(license, config) .process({ value: await fs.readFile(inputUrl),