Website Structure

This commit is contained in:
supalerk-ar66 2026-01-13 10:46:40 +07:00
parent 62812f2090
commit 71f0676a62
22365 changed files with 4265753 additions and 791 deletions

View file

@ -0,0 +1,782 @@
# CHANGES for `@es-joy/jsdoccomment`
## 0.78.0
- chore: update typescript-eslint/types, devDeps.
## 0.77.0
- chore: update typescript-eslint/types, jsdoc-type-pratt-parser, devDeps.
## 0.76.0
- chore: update jsdoc-type-pratt-parser, devDep.
## 0.75.0
- chore: update jsdoc-type-pratt-parser
## 0.74.0
- chore: update jsdoc-type-pratt-parser
## 0.73.0
- chore: update jsdoc-type-pratt-parser
## 0.72.0
- chore: update jsdoc-type-pratt-parser and devDep.
## 0.71.0
- chore: update typescript-eslint/types, jsdoc-type-pratt-parser, devDeps.
## 0.70.1
- chore: update jsdoc-type-pratt-parser
## 0.70.0
- chore: update jsdoc-type-pratt-parser and devDep.
- feat(demo): syntax highlight JSDoc and JSON
## 0.69.0
- chore: update jsdoc-type-pratt-parser and devDep.
## 0.68.1
- fix(typescript): ensure options arguments are optional
## 0.68.0
- feat: allow provision of `jsdoc-type-pratt-parser` options to `commentHandler` and `estreeToString`
## 0.67.3
- chore: update jsdoc-type-pratt-parser and devDep.
## 0.67.2
- chore: update jsdoc-type-pratt-parser and devDep.
## 0.67.1
- chore: update jsdoc-type-pratt-parser
## 0.67.0
- chore: update jsdoc-type-pratt-parser
## 0.66.0
- chore: update jsdoc-type-pratt-parser
## 0.65.2
- chore: update jsdoc-type-pratt-parser
## 0.65.1
- chore: update jsdoc-type-pratt-parser and devDeps.
## 0.65.0
- chore: update typescript-eslint/types, jsdoc-type-pratt-parser and devDeps.
## 0.64.0
- chore: update jsdoc-type-pratt-parser and devDeps.
## 0.63.0
- chore: update jsdoc-type-pratt-parser and devDeps.
## 0.62.0
- chore: update jsdoc-type-pratt-parser and devDeps.
## 0.61.0
- chore: update jsdoc-type-pratt-parser and devDeps.
## 0.60.0
- feat(`commentParserToESTree`): default to typescript mode
- feat(`parseComment`): give more specific error when fails to parse
- feat: add demo
## 0.59.0
- chore: update @typescript-eslint/types and devDeps
## 0.58.0
- feat: update @typescript-eslint/types, jsdoc-type-pratt-parser and devDeps.
## 0.57.0
- feat: update jsdoc-type-pratt-parser and devDeps.
## 0.56.0
- feat: update jsdoc-type-pratt-parser and devDeps.
## 0.55.0
- chore: update typescript-eslint/types, jsdoc-type-pratt-parser and devDeps.
## 0.54.1
- fix: make optional overload checks include `ExportNamedDeclaration`s.
## 0.54.0
- feat: make overload checks optional
- chore: switch to Node 20 as Babel target and remove unneeded plugin
- chore: update devDeps.
## 0.53.0
- chore: upgrade typescript-eslint/types, jsdoc-type-pratt-parser, devDeps
## 0.52.0
BREAKING CHANGES:
- chore: require Node >= 20.11.0
## 0.51.1
- fix(`jsdoccomment`): ensure overloading check looks to identically named functions only
## 0.51.0
- fix(`jsdoccomment`): find overload comment
- chore: update types/estree, typescript-eslint/types, devDeps.
## 0.50.2
- fix: remove `@types/eslint` (#20)
Thanks:
- [@ocavue](https://github.com/ocavue)
## 0.50.1
- fix: for `TSFunctionType` part of `TSTypeAliasDeclaration` and export, return export
## 0.50.0
BREAKING CHANGES:
- Require Node 18+
- chore: add `.d.cts` file / add type dependencies (#19)
- test(ci): drop Node 16.x and add Node 22.x
Thanks:
- [@typhonrt](https://github.com/typhonrt)
## 0.49.0
- fix: avoid changing `name` for `@template`; should be able to recover
optional brackets and defaults in AST
## 0.48.0
- chore: bump jsdoc-type-pratt-parser and devDeps.
## 0.47.0
- fix(`parseComment`): assume closing bracket of name is final instead of
first one
- chore: flat config/ESLint 9; change browser targets; lint; update devDeps.
## 0.46.0
- chore: update esquery, drop bundling of types, update devDeps
## 0.45.0
- feat: get following comment (experimental)
## 0.44.0
- feat: add `getNonJsdocComment` for getting non-JSDoc comments above node
## 0.43.1
- fix: for `@template` name parsing, ensure (default-)bracketed name is not broken with internal spaces.
## 0.43.0
This release brings surgical round trip parsing to generated AST and reconstruction of JSDoc comment blocks via: `parseComment` ->
`commentParserToESTree` -> `estreeToString`.
- feat: new option `spacing` for `commentParserToESTree`; the default is `compact` removing empty description lines.
Set to `preserve` to retain empty description lines.
- feat: new properties in the `JsdocBlock` generated AST `delimiterLineBreak` and `preterminalLineBreak` that encode
any line break after the opening `delimiter` and before the closing `terminal` string. Values are either `\n` or an
empty string.
- chore: update devDeps / switch to Vitest.
- New [API documentation](https://es-joy.github.io/jsdoccomment/).
Thanks:
- [@typhonrt](https://github.com/typhonrt)
## 0.42.0
- feat: expand argument for `parseComment` to accept a comment token string ([@typhonrt](https://github.com/typhonrt))
- chore: update devDeps.
## 0.41.0
- feat: look above surrounding parenthesis tokens for comment blocks, even if on a higher line than the corresponding AST structure
- chore: update comment-parser and devDeps.
## 0.40.1
- chore(TS): fix path issue
## 0.40.0
- chore: update comment-parser and devDeps.
- chore(TS): switch to NodeNext
## 0.39.4
- fix: include type exports for full inlineTags (and line) property support on blocks and tags
## 0.39.3
- fix: add type details for Node range and settings
## 0.39.2
- fix: export additional typedefs from index.js
## 0.39.1
- fix: typing export
## 0.39.0
- feat: types for test files and emit declaration files
- fix(estreeToString): add `JsdodInlineTag` stringify support
- refactor: lint
- docs: add `JsdocInlineTag` to README
- chore: update devDeps.
## 0.38.0
- feat: add parsing inline tags (#12); fixes #11
## 0.37.1
- chore: support Node 20
- chore: update esquery, devDeps.
## 0.37.0
## 0.37.0-pre.0
- fix: update `jsdoc-type-pratt-parser` (supports bracket indexes)
## 0.36.1
- fix(`getReducedASTNode`): stop checking for comment blocks at return
statement
## 0.36.0
- feat: add `hasPreterminalTagDescription` property
- fix: avoid description line properties if tag is present
- fix: ensure description and description lines added to terminal multi-line tag
## 0.35.0
- feat: add `hasPreterminalDescription` property
- fix: allow newline even for 1st line (after 0th)
## 0.34.0
- feat: add `descriptionStartLine` and `descriptionEndLine` properties
- fix: avoid duplication with 0 line comments
- chore: update devDeps.
## 0.33.4
- chore: republish as npm seems to have missed the release
## 0.33.3
- fix: ensure multi-line `description` includes newline except for
initial line descriptions
## 0.33.2
- fix: avoid repetition within multi-line descriptions
## 0.33.1
- fix: add to default no types: `description`, `example`, `file`,
`fileoverview`, `license`, `overview`, `see`, `summary`
- fix: add to no names: `file`, `fileoverview, `overview`
## 0.33.0
- chore: add Node 19 to `engines` (@RodEsp)
- chore: update devDeps. and build file accordingly
## 0.32.0
- feat: have comment checking stop at assignment patterns (comments for
defaults should not rise to function itself)
- chore: bump devDeps.
## 0.31.0
- feat: support default values with `@template` per
<https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html#template>
## 0.30.0
- chore: bump `jsdoc-type-pratt-parser` and devDeps.
## 0.29.0
- fix: update `engines` as per current `getJSDocComment` behavior
- chore: update devDeps.
## 0.28.1
- fix(`getReducedASTNode`): token checking
- build: add Node 18 support (@WikiRik)
## 0.28.0
- chore: bump `engines` to support Node 18
## 0.27.0
- chore: bump `jsdoc-type-pratt-parser` and devDeps.
## 0.26.1
- fix(`estreeToString`): ensure `typeLines` may be picked up
## 0.26.0
- feat(`getJSDocComment`): allow function to detect comments just preceding a
parenthesized expression (these have no special AST but their tokens
have to be overpassed)
## 0.25.0
- feat(`parseComment`): properly support whitespace
- fix(`estreeToString`): carriage return placement for ending of JSDoc block
- fix(`commentParserToESTree`): avoid adding initial space before a tag if on
a single line
- test: make tests more accurate to jsdoc semantically
## 0.24.0
- feat(`estreeToString`): support stringification of `parsedType` but with
a new `preferRawType` option allowing the old behavior of using `rawType`
## 0.23.6
- fix(`commentParserToESTree`): ensure `postType` added after multi-line type
- fix(`estreeToString`): ensure `JsdocTypeLine` stringified with `initial` and
that they are joined together with newlines
## 0.23.5
- fix(`commentParserToESTree`): avoid duplicating tag names
## 0.23.4
- fix(`estreeToString`): add `delimiter`, etc. if adding `JsdocDescriptionLine`
for `JsdocBlock`
- fix(`estreeToString`): add line break when tags are present (unless already
ending in newline)
## 0.23.3
- fix(`estreeToString`): handle multi-line block descriptions followed by
tags with line break
## 0.23.2
- fix: ensure JsdocBlock stringifier has any initial whitespace on end line
## 0.23.1
- docs(README): update
## 0.23.0
- BREAKING CHANGE(`commentParserToESTree`): rename `start` and `end` to
`initial` and `terminal` to avoid any conflicts with Acorn-style parsers
- feat: add `initial` and `terminal` on `JsdocBlock`
## 0.22.2
- fix: preserve type tokens
- perf: cache tokenizers
## 0.22.1
- fix: ensure `getJSDocComment` does not treat block comments as JSDoc unless
their first asterisk is followed by whitespace
## 0.22.0
- fix: update dep. `jsdoc-type-pratt-parser`
- chore: update `comment-parser` and simplify as possible
## 0.21.2
- fix: only throw if the raw type is not empty
## 0.21.1
- fix: provide clearer error message for `throwOnTypeParsingErrors`
## 0.21.0
- feat: add `throwOnTypeParsingErrors` to receive run-time type parsing errors
for `parsedType`
- chore: update jsdoc-type-pratt-parser and devDeps.; also lints
## 0.20.1
- fix: resume catching bad parsed type (at least until
`jsdoc-type-pratt-parser` may support all expected types)
## 0.20.0
- feat: add estree stringifer
- fix: properly supports `name`/`postName` for multi-line type
- fix: allow pratt parser to fail (unless empty)
- fix: don't add tag postDelimiter when on 0 description line
- fix: avoid adding extra line when only name and no succeeding description
- docs: clarify re: `kind`
- test: add `parsedType` with correct mode; add tests
- chore: updates jsdoc-type-pratt-parser
- chore: updates devDeps.
## 0.19.0
### User-impacting
- feat: treat `@kind` as having no name
### Dev-impacting
- docs: jsdoc
- test: begin checking `jsdoccomment`
- test: adds lcov reporter and open script for it
- chore: update devDeps.
## 0.18.0
### User-impacting
- feat: add non-visitable `endLine` property (so can detect line number
when no description present)
- feat: supply `indent` default for `parseComment`
- fix: ensure `postName` gets a space for `@template` with a description
- fix: converting JSDoc comment with tag on same line as end (e.g., single
line) to AST
- chore: update `jsdoc-type-pratt-parser`
### Dev-impacting
- docs: add jsdoc blocks internally
- chore: update devDeps.
- test: avoid need for `expect`
- test: complete coverage for `commentHandler`, `parseComment` tests
## 0.17.0
### User-impacting
- Enhancement: Re-export `jsdoc-type-pratt-parser`
- Update: `jsdoc-type-pratt-parser` to 2.2.1
### Dev-impacting
- npm: Update devDeps.
## 0.16.0
### User-impacting
- Update: `jsdoc-type-pratt-parser` to 2.2.0
### Dev-impacting
- npm: Update devDeps.
## 0.15.0
### User-impacting
- Update: `jsdoc-type-pratt-parser` to 2.1.0
### Dev-impacting
- npm: Update devDeps.
## 0.14.2
### User-impacting
- Fix: Find comments previous to parentheses (used commonly in TypeScript)
### Dev-impacting
- npm: Update devDeps.
## 0.14.1
### User-impacting
- Update: `jsdoc-type-pratt-parser` to 2.0.2
## 0.14.0
### User-impacting
- Update: `jsdoc-type-pratt-parser` to 2.0.1
### Dev-impacting
- npm: Update devDeps.
## 0.13.0
### User-impacting
- Update: `comment-parser` to 1.3.0
- Fix: Allow comment on `ExportDefaultDeclaration`
## 0.12.0
### User-impacting
- Update: `jsdoc-type-pratt-parser` to 2.0.0
- Enhancement: Support Node 17 (@timgates42)
- Docs: Typo (@timgates42)
### Dev-impacting
- Linting: As per latest ash-nazg
- npm: Update devDeps.
## 0.11.0
- Update: For `@typescript/eslint-parser@5`, add `PropertyDefinition`
## 0.10.8
### User-impacting
- npm: Liberalize `engines` as per `comment-parser` change
- npm: Bump `comment-parser`
### Dev-impacting
- Linting: As per latest ash-nazg
- npm: Update devDeps.
## 0.10.7
- npm: Update comment-parser with CJS fix and re-exports
- npm: Update devDeps.
## 0.10.6
- Fix: Ensure copying latest build of `comment-parser`'s ESM utils
## 0.10.5
- npm: Bump fixed `jsdoc-type-pratt-parser` and devDeps.
## 0.10.4
- Fix: Bundle `comment-parser` nested imports so that IDEs (like Atom)
bundling older Node versions can still work. Still mirroring the
stricter `comment-parser` `engines` for now, however.
## 0.10.3
- npm: Avoid exporting nested subpaths for sake of older Node versions
## 0.10.2
- npm: Specify exact supported range: `^12.20 || ^14.14.0 || ^16`
## 0.10.1
- npm: Apply patch version of `comment-parser`
## 0.10.0
- npm: Point to stable `comment-parser`
## 0.9.0-alpha.6
### User-impacting
- Update: For `comment-parser` update, add `lineEnd`
## 0.9.0-alpha.5
### User-impacting
- npm: Bump `comment-parser` (for true ESM)
- Update: Remove extensions for packages for native ESM in `comment-parser` fix
### Dev-impacting
- npm: Update devDeps.
## 0.9.0-alpha.4
- Docs: Update repo info in `package.json`
## 0.9.0-alpha.3
- Fix: Due to `comment-parser` still needing changes, revert for now to alpha.1
## 0.9.0-alpha.2
### User-impacting
- npm: Bump `comment-parser` (for true ESM)
- Update: Remove extensions for packages for native ESM in `comment-parser` fix
### Dev-impacting
- npm: Update devDeps.
## 0.9.0-alpha.1
### User-impacting
- Breaking change: Indicate minimum for `engines` as Node >= 12
- npm: Bump `comment-parser`
### Dev-impacting
- npm: Lint cjs files
- npm: Fix eslint script
- npm: Update devDeps.
## 0.8.0
### User-impacting
- npm: Update `jsdoc-type-pratt-parser` (prerelease to stable patch)
### Dev-impacting
- npm: Update devDeps.
## 0.8.0-alpha.2
- Fix: Avoid erring with missing `typeLines`
## 0.8.0-alpha.1
- Breaking change: Export globally as `JsdocComment`
- Breaking change: Change `JSDoc` prefixes of all node types to `Jsdoc`
- Breaking change: Drop `jsdoctypeparserToESTree`
- Breaking enhancement: Switch to `jsdoc-type-pratt-parser` (toward greater
TypeScript expressivity and compatibility/support with catharsis)
- Enhancement: Export `jsdocTypeVisitorKeys` (from `jsdoc-type-pratt-parser`)
## 0.7.2
- Fix: Add `@description` to `noNames`
## 0.7.1
- Fix: Add `@summary` to `noNames`
## 0.7.0
- Enhancement: Allow specifying `noNames` and `noTypes` on `parseComment`
to override (or add to) tags which should have no names or types.
- Enhancement: Export `hasSeeWithLink` utility and `defaultNoTypes` and
`defaultNoNames`.
## 0.6.0
- Change `comment-parser` `tag` AST to avoid initial `@`
## 0.5.1
- Fix: Avoid setting `variation` name (just the description) (including in
dist)
- npm: Add `prepublishOnly` script
## 0.5.0
- Fix: Avoid setting `variation` name (just the description)
## 0.4.4
- Fix: Avoid setting `name` and `description` for simple `@template SomeName`
## 0.4.3
- npm: Ignores Github file
## 0.4.2
- Fix: Ensure replacement of camel-casing (used in `jsdoctypeparser` nodes and
visitor keys is global. The practical effect is that
`JSDocTypeNamed_parameter` -> `JSDocTypeNamedParameter`,
`JSDocTypeRecord_entry` -> `JSDocTypeRecordEntry`
`JSDocTypeNot_nullable` -> `JSDocTypeNotNullable`
`JSDocTypeInner_member` -> `JSDocTypeInnerMember`
`JSDocTypeInstance_member` -> `JSDocTypeInstanceMember`
`JSDocTypeString_value` -> `JSDocTypeStringValue`
`JSDocTypeNumber_value` -> `JSDocTypeNumberValue`
`JSDocTypeFile_path` -> `JSDocTypeFilePath`
`JSDocTypeType_query` -> `JSDocTypeTypeQuery`
`JSDocTypeKey_query` -> `JSDocTypeKeyQuery`
- Fix: Add missing `JSDocTypeLine` to visitor keys
- Docs: Explain AST structure/differences
## 0.4.1
- Docs: Indicate available methods with brief summary on README
## 0.4.0
- Enhancement: Expose `parseComment` and `getTokenizers`.
## 0.3.0
- Enhancement: Expose `toCamelCase` as new method rather than within a
utility file.
## 0.2.0
- Enhancement: Exposes new methods: `commentHandler`,
`commentParserToESTree`, `jsdocVisitorKeys`, `jsdoctypeparserToESTree`,
`jsdocTypeVisitorKeys`,
## 0.1.1
- Build: Add Babel to work with earlier Node
## 0.1.0
- Initial version

View file

@ -0,0 +1,20 @@
Copyright JS Foundation and other contributors, https://js.foundation
Copyright (c) 2021 Brett Zamir
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,242 @@
# @es-joy/jsdoccomment
[![NPM](https://img.shields.io/npm/v/@es-joy/jsdoccomment.svg?label=npm)](https://www.npmjs.com/package/@es-joy/jsdoccomment)
[![License](https://img.shields.io/badge/license-MIT-yellowgreen.svg?style=flat)](https://github.com/es-joy/jsdoccomment/blob/main/LICENSE-MIT.txt)
[![Build Status](https://github.com/es-joy/jsdoccomment/workflows/CI/CD/badge.svg)](#)
[![API Docs](https://img.shields.io/badge/API%20Documentation-476ff0)](https://es-joy.github.io/jsdoccomment/docs/)
See the **[Demo](https://es-joy.github.io/jsdoccomment/docs/)**.
This project aims to preserve and expand upon the
`SourceCode#getJSDocComment` functionality of the deprecated ESLint method.
It also exports a number of functions currently for working with JSDoc:
## API
### `parseComment`
For parsing `comment-parser` in a JSDoc-specific manner.
Might wish to have tags with or without tags, etc. derived from a split off
JSON file.
### `commentParserToESTree`
Converts [comment-parser](https://github.com/syavorsky/comment-parser)
AST to ESTree/ESLint/Babel friendly AST. See the "ESLint AST..." section below.
### `estreeToString`
Stringifies. In addition to the node argument, it accepts an optional second
options object with a single `preferRawType` key. If you don't need to modify
JSDoc type AST, you might wish to set this to `true` to get the benefits of
preserving the raw form, but for AST-based stringification of JSDoc types,
keep it `false` (the default).
### `jsdocVisitorKeys`
The [VisitorKeys](https://github.com/eslint/eslint-visitor-keys)
for `JsdocBlock`, `JsdocDescriptionLine`, and `JsdocTag`. More likely to be
subject to change or dropped in favor of another type parser.
### `jsdocTypeVisitorKeys`
Just a re-export of [VisitorKeys](https://github.com/eslint/eslint-visitor-keys)
from [`jsdoc-type-pratt-parser`](https://github.com/simonseyock/jsdoc-type-pratt-parser/).
### `getDefaultTagStructureForMode`
Provides info on JSDoc tags:
- `nameContents` ('namepath-referencing'|'namepath-defining'|
'dual-namepath-referencing'|false) - Whether and how a name is allowed
following any type. Tags without a proper name (value `false`) may still
have a description (which can appear like a name); `descriptionAllowed`
in such cases would be `true`.
The presence of a truthy `nameContents` value is therefore only intended
to signify whether separate parsing should occur for a name vs. a
description, and what its nature should be.
- `nameRequired` (boolean) - Whether a name must be present following any type.
- `descriptionAllowed` (boolean) - Whether a description (following any name)
is allowed.
- `typeAllowed` (boolean) - Whether the tag accepts a curly bracketed portion.
Even without a type, a tag may still have a name and/or description.
- `typeRequired` (boolean) - Whether a curly bracketed type must be present.
- `typeOrNameRequired` (boolean) - Whether either a curly bracketed type is
required or a name, but not necessarily both.
### Miscellaneous
Also currently exports these utilities:
- `getTokenizers` - Used with `parseComment` (its main core).
- `hasSeeWithLink` - A utility to detect if a tag is `@see` and has a `@link`.
- `commentHandler` - Used by `eslint-plugin-jsdoc`.
- `commentParserToESTree`- Converts [comment-parser](https://github.com/syavorsky/comment-parser)
AST to ESTree/ESLint/Babel friendly AST.
- `jsdocVisitorKeys` - The [VisitorKeys](https://github.com/eslint/eslint-visitor-keys)
for `JSDocBlock`, `JSDocDescriptionLine`, and `JSDocTag`.
- `jsdocTypeVisitorKeys` - [VisitorKeys](https://github.com/eslint/eslint-visitor-keys)
for `jsdoc-type-pratt-parser`.
- `defaultNoTypes` = The tags which allow no types by default:
`default`, `defaultvalue`, `description`, `example`, `file`,
`fileoverview`, `license`, `overview`, `see`, `summary`
- `defaultNoNames` - The tags which allow no names by default:
`access`, `author`, `default`, `defaultvalue`, `description`, `example`,
`exception`, `file`, `fileoverview`, `kind`, `license`, `overview`,
`return`, `returns`, `since`, `summary`, `throws`, `version`, `variation`
## ESLint AST produced for `comment-parser` nodes (`JsdocBlock`, `JsdocTag`, and `JsdocDescriptionLine`)
Note: Although not added in this package, `@es-joy/jsdoc-eslint-parser` adds
a `jsdoc` property to other ES nodes (using this project's `getJSDocComment`
to determine the specific comment-block that will be attached as AST).
### `JsdocBlock`
Has the following visitable properties:
1. `descriptionLines` (an array of `JsdocDescriptionLine` for multiline
descriptions).
2. `tags` (an array of `JsdocTag`; see below)
3. `inlineTags` (an array of `JsdocInlineTag`; see below)
Has the following custom non-visitable property:
1. `delimiterLineBreak` - A string containing any line break after `delimiter`.
2. `lastDescriptionLine` - A number
3. `endLine` - A number representing the line number with `end`/`terminal`
4. `descriptionStartLine` - A 0+ number indicating the line where any
description begins
5. `descriptionEndLine` - A 0+ number indicating the line where the description
ends
6. `hasPreterminalDescription` - Set to 0 or 1. On if has a block description
on the same line as the terminal `*/`.
7. `hasPreterminalTagDescription` - Set to 0 or 1. On if has a tag description
on the same line as the terminal `*/`.
8. `preterminalLineBreak` - A string containing any line break before `terminal`.
May also have the following non-visitable properties from `comment-parser`:
1. `description` - Same as `descriptionLines` but as a string with newlines.
2. `delimiter`
3. `postDelimiter`
4. `lineEnd`
5. `initial` (from `start`)
6. `terminal` (from `end`)
### `JsdocTag`
Has the following visitable properties:
1. `parsedType` (the `jsdoc-type-pratt-parser` AST representation of the tag's
type (see the `jsdoc-type-pratt-parser` section below)).
2. `typeLines` (an array of `JsdocTypeLine` for multiline type strings)
3. `descriptionLines` (an array of `JsdocDescriptionLine` for multiline
descriptions)
4. `inlineTags` (an array of `JsdocInlineTag`)
May also have the following non-visitable properties from `comment-parser`
(note that all are included from `comment-parser` except `end` as that is only
for JSDoc blocks and note that `type` is renamed to `rawType` and `start` to
`initial`):
1. `description` - Same as `descriptionLines` but as a string with newlines.
2. `rawType` - `comment-parser` has this named as `type`, but because of a
conflict with ESTree using `type` for Node type, we renamed it to
`rawType`. It is otherwise the same as in `comment-parser`, i.e., a string
with newlines, though with the initial `{` and final `}` stripped out.
See `typeLines` for the array version of this property.
3. `initial` - Renamed from `start` to avoid potential conflicts with
Acorn-style parser processing tools
4. `delimiter`
5. `postDelimiter`
6. `tag` (this does differ from `comment-parser` now in terms of our stripping
the initial `@`)
7. `postTag`
8. `name`
9. `postName`
10. `postType`
### `JsdocDescriptionLine`
No visitable properties.
May also have the following non-visitable properties from `comment-parser`:
1. `delimiter`
2. `postDelimiter`
3. `initial` (from `start`)
4. `description`
### `JsdocTypeLine`
No visitable properties.
May also have the following non-visitable properties from `comment-parser`:
1. `delimiter`
2. `postDelimiter`
3. `initial` (from `start`)
4. `rawType` - Renamed from `comment-parser` to avoid a conflict. See
explanation under `JsdocTag`
### `JsdocInlineTag`
No visitable properties.
Has the following non-visitable properties:
1. `format`: 'pipe' | 'plain' | 'prefix' | 'space'. These follow the styles of [link](https://jsdoc.app/tags-inline-link.html) or [tutorial](https://jsdoc.app/tags-inline-tutorial.html).
1. `pipe`: `{@link namepathOrURL|link text}`
2. `plain`: `{@link namepathOrURL}`
3. `prefix`: `[link text]{@link namepathOrURL}`
4. `space`: `{@link namepathOrURL link text (after the first space)}`
2. `namepathOrURL`: string
3. `tag`: string. The standard allows `tutorial` or `link`
4. `text`: string
## ESLint AST produced for `jsdoc-type-pratt-parser`
The AST, including `type`, remains as is from [jsdoc-type-pratt-parser](https://github.com/simonseyock/jsdoc-type-pratt-parser/).
The type will always begin with a `JsdocType` prefix added, along with a
camel-cased type name, e.g., `JsdocTypeUnion`.
The `jsdoc-type-pratt-parser` visitor keys are also preserved without change.
You can get a sense of the structure of these types using the parser's
[tester](https://jsdoc-type-pratt-parser.github.io/jsdoc-type-pratt-parser/).
## Installation
```shell
npm i @es-joy/jsdoccomment
```
## Changelog
The changelog can be found on the [CHANGES.md](https://github.com/es-joy/jsdoccomment/blob/main/CHANGES.md).
<!--## Contributing
Everyone is welcome to contribute. Please take a moment to review the [contributing guidelines](CONTRIBUTING.md).
-->
## Authors and license
[Brett Zamir](http://brett-zamir.me/) and
[contributors](https://github.com/es-joy/jsdoccomment/graphs/contributors).
MIT License, see the included [LICENSE-MIT.txt](https://github.com/es-joy/jsdoccomment/blob/main/LICENSE-MIT.txt) file.
## To-dos
1. Get complete code coverage
1. Given that `esquery` expects a `right` property to search for `>` (the
child selector), we should perhaps insist, for example, that params are
the child property for `JsdocBlock` or such. Where `:has()` is currently
needed, one could thus instead just use `>`.
1. Might add `trailing` for `JsdocBlock` to know whether it is followed by a
line break or what not; `comment-parser` does not provide, however
1. Fix and properly utilize `indent` argument (challenging for
`eslint-plugin-jsdoc` but needed for `jsdoc-eslint-parser` stringifiers
to be more faithful); should also then use the proposed `trailing` as well

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,403 @@
import * as estree from 'estree';
import * as comment_parser from 'comment-parser';
import * as jsdoc_type_pratt_parser from 'jsdoc-type-pratt-parser';
export * from 'jsdoc-type-pratt-parser';
export { visitorKeys as jsdocTypeVisitorKeys } from 'jsdoc-type-pratt-parser';
import * as _typescript_eslint_types from '@typescript-eslint/types';
import * as eslint from 'eslint';
type JsdocTypeLine = {
delimiter: string;
postDelimiter: string;
rawType: string;
initial: string;
type: 'JsdocTypeLine';
};
type JsdocDescriptionLine = {
delimiter: string;
description: string;
postDelimiter: string;
initial: string;
type: 'JsdocDescriptionLine';
};
type JsdocInlineTagNoType = {
format: 'pipe' | 'plain' | 'prefix' | 'space';
namepathOrURL: string;
tag: string;
text: string;
};
type JsdocInlineTag = JsdocInlineTagNoType & {
type: 'JsdocInlineTag';
};
type JsdocTag = {
delimiter: string;
description: string;
descriptionLines: JsdocDescriptionLine[];
initial: string;
inlineTags: JsdocInlineTag[];
name: string;
postDelimiter: string;
postName: string;
postTag: string;
postType: string;
rawType: string;
parsedType: jsdoc_type_pratt_parser.RootResult | null;
tag: string;
type: 'JsdocTag';
typeLines: JsdocTypeLine[];
};
type Integer = number;
type JsdocBlock = {
delimiter: string;
delimiterLineBreak: string;
description: string;
descriptionEndLine?: Integer;
descriptionLines: JsdocDescriptionLine[];
descriptionStartLine?: Integer;
hasPreterminalDescription: 0 | 1;
hasPreterminalTagDescription?: 1;
initial: string;
inlineTags: JsdocInlineTag[];
lastDescriptionLine?: Integer;
endLine: Integer;
lineEnd: string;
postDelimiter: string;
tags: JsdocTag[];
terminal: string;
preterminalLineBreak: string;
type: 'JsdocBlock';
};
type JtppOptions = {
module?: boolean;
strictMode?: boolean;
asyncFunctionBody?: boolean;
classContext?: boolean;
computedPropertyParser?: (text: string, options?: any) => unknown;
};
type CommentParserToESTreeOptions = {
/**
* By default, empty lines are
* compacted; set to 'preserve' to preserve empty comment lines.
*/
spacing?: 'compact' | 'preserve';
throwOnTypeParsingErrors?: boolean;
jsdocTypePrattParserArgs?: JtppOptions;
};
/**
* @typedef {{
* module?: boolean;
* strictMode?: boolean;
* asyncFunctionBody?: boolean;
* classContext?: boolean;
* computedPropertyParser?: (text: string, options?: any) => unknown;
* }} JtppOptions
*/
/**
* @typedef {object} CommentParserToESTreeOptions
* @property {'compact'|'preserve'} [spacing] By default, empty lines are
* compacted; set to 'preserve' to preserve empty comment lines.
* @property {boolean} [throwOnTypeParsingErrors]
* @property {JtppOptions} [jsdocTypePrattParserArgs]
*/
/**
* Converts comment parser AST to ESTree format.
* @param {import('.').JsdocBlockWithInline} jsdoc
* @param {import('jsdoc-type-pratt-parser').ParseMode} mode
* @param {CommentParserToESTreeOptions} [opts]
* @returns {JsdocBlock}
*/
declare function commentParserToESTree(
jsdoc: JsdocBlockWithInline,
mode?: jsdoc_type_pratt_parser.ParseMode,
{ spacing, throwOnTypeParsingErrors, jsdocTypePrattParserArgs }?: CommentParserToESTreeOptions,
): JsdocBlock;
declare namespace jsdocVisitorKeys {
let JsdocBlock: string[];
let JsdocDescriptionLine: any[];
let JsdocTypeLine: any[];
let JsdocTag: string[];
let JsdocInlineTag: any[];
}
/**
* @param {{
* mode: import('jsdoc-type-pratt-parser').ParseMode,
* [key: string]: any
* }} settings
* @param {import('./commentParserToESTree.js').
* CommentParserToESTreeOptions} [commentParserToESTreeOptions]
* @returns {import('.').CommentHandler}
*/
declare function commentHandler(
settings: {
mode: jsdoc_type_pratt_parser.ParseMode;
[key: string]: any;
},
commentParserToESTreeOptions?: CommentParserToESTreeOptions,
): CommentHandler;
/**
* @todo convert for use by escodegen (until may be patched to support
* custom entries?).
* @param {import('./commentParserToESTree').JsdocBlock|
* import('./commentParserToESTree').JsdocDescriptionLine|
* import('./commentParserToESTree').JsdocTypeLine|
* import('./commentParserToESTree').JsdocTag|
* import('./commentParserToESTree').JsdocInlineTag|
* import('jsdoc-type-pratt-parser').RootResult
* } node
* @param {import('.').ESTreeToStringOptions} [opts]
* @throws {Error}
* @returns {string}
*/
declare function estreeToString(
node:
| JsdocBlock
| JsdocDescriptionLine
| JsdocTypeLine
| JsdocTag
| JsdocInlineTag
| jsdoc_type_pratt_parser.RootResult,
opts?: ESTreeToStringOptions,
): string;
type Token =
| eslint.AST.Token
| estree.Comment
| {
type: eslint.AST.TokenType | 'Line' | 'Block' | 'Shebang';
range: [number, number];
value: string;
};
type ESLintOrTSNode = eslint.Rule.Node | _typescript_eslint_types.TSESTree.Node;
type int = number;
type DecoratedNode =
| ESLintOrTSNode
| estree.Comment
| (eslint.Rule.Node & {
declaration?: any;
decorators?: any[];
});
/**
* Reduces the provided node to the appropriate node for evaluating
* JSDoc comment status.
*
* @param {ESLintOrTSNode} node An AST node.
* @param {import('eslint').SourceCode} sourceCode The ESLint SourceCode.
* @returns {ESLintOrTSNode} The AST node that
* can be evaluated for appropriate JSDoc comments.
*/
declare function getReducedASTNode(node: ESLintOrTSNode, sourceCode: eslint.SourceCode): ESLintOrTSNode;
/**
* Retrieves the JSDoc comment for a given node.
*
* @param {import('eslint').SourceCode} sourceCode The ESLint SourceCode
* @param {import('eslint').Rule.Node} node The AST node to get
* the comment for.
* @param {{maxLines: int, minLines: int, [name: string]: any}} settings The
* settings in context
* @param {{checkOverloads?: boolean}} [opts]
* @returns {Token|null} The Block comment
* token containing the JSDoc comment for the given node or
* null if not found.
* @public
*/
declare function getJSDocComment(
sourceCode: eslint.SourceCode,
node: eslint.Rule.Node,
settings: {
maxLines: int;
minLines: int;
[name: string]: any;
},
opts?: {
checkOverloads?: boolean;
},
): Token | null;
/**
* Retrieves the comment preceding a given node.
*
* @param {import('eslint').SourceCode} sourceCode The ESLint SourceCode
* @param {ESLintOrTSNode} node The AST node to get
* the comment for.
* @param {{maxLines: int, minLines: int, [name: string]: any}} settings The
* settings in context
* @returns {Token|null} The Block comment
* token containing the JSDoc comment for the given node or
* null if not found.
* @public
*/
declare function getNonJsdocComment(
sourceCode: eslint.SourceCode,
node: ESLintOrTSNode,
settings: {
maxLines: int;
minLines: int;
[name: string]: any;
},
): Token | null;
/**
* @typedef {(
* ESLintOrTSNode|
* import('estree').Comment|
* import('eslint').Rule.Node & {declaration?: any, decorators?: any[]}
* )} DecoratedNode
*/
/**
* @param {DecoratedNode} node
* @returns {import('@typescript-eslint/types').TSESTree.Decorator|undefined}
*/
declare function getDecorator(node: DecoratedNode): _typescript_eslint_types.TSESTree.Decorator | undefined;
/**
* Checks for the presence of a JSDoc comment for the given node and returns it.
*
* @param {ESLintOrTSNode} astNode The AST node to get
* the comment for.
* @param {import('eslint').SourceCode} sourceCode
* @param {{maxLines: int, minLines: int, [name: string]: any}} settings
* @param {{nonJSDoc?: boolean}} [opts]
* @returns {Token|null} The Block comment token containing the JSDoc comment
* for the given node or null if not found.
*/
declare function findJSDocComment(
astNode: ESLintOrTSNode,
sourceCode: eslint.SourceCode,
settings: {
maxLines: int;
minLines: int;
[name: string]: any;
},
opts?: {
nonJSDoc?: boolean;
},
): Token | null;
/**
* Checks for the presence of a comment following the given node and
* returns it.
*
* This method is experimental.
*
* @param {import('eslint').SourceCode} sourceCode
* @param {ESLintOrTSNode} astNode The AST node to get
* the comment for.
* @returns {Token|null} The comment token containing the comment
* for the given node or null if not found.
*/
declare function getFollowingComment(sourceCode: eslint.SourceCode, astNode: ESLintOrTSNode): Token | null;
declare function hasSeeWithLink(spec: comment_parser.Spec): boolean;
declare const defaultNoTypes: string[];
declare const defaultNoNames: string[];
/**
* Can't import `comment-parser/es6/parser/tokenizers/index.js`,
* so we redefine here.
*/
type CommentParserTokenizer = (spec: comment_parser.Spec) => comment_parser.Spec;
/**
* Can't import `comment-parser/es6/parser/tokenizers/index.js`,
* so we redefine here.
* @typedef {(spec: import('comment-parser').Spec) =>
* import('comment-parser').Spec} CommentParserTokenizer
*/
/**
* @param {object} [cfg]
* @param {string[]} [cfg.noTypes]
* @param {string[]} [cfg.noNames]
* @returns {CommentParserTokenizer[]}
*/
declare function getTokenizers({
noTypes,
noNames,
}?: {
noTypes?: string[];
noNames?: string[];
}): CommentParserTokenizer[];
/**
* Accepts a comment token or complete comment string and converts it into
* `comment-parser` AST.
* @param {string | {value: string}} commentOrNode
* @param {string} [indent] Whitespace
* @returns {import('.').JsdocBlockWithInline}
*/
declare function parseComment(
commentOrNode:
| string
| {
value: string;
},
indent?: string,
): JsdocBlockWithInline;
/**
* Splits the `{@ prefix}` from remaining `Spec.lines[].token.description`
* into the `inlineTags` tokens, and populates `spec.inlineTags`
* @param {import('comment-parser').Block} block
* @returns {import('.').JsdocBlockWithInline}
*/
declare function parseInlineTags(block: comment_parser.Block): JsdocBlockWithInline;
type InlineTag = JsdocInlineTagNoType & {
start: number;
end: number;
};
type JsdocTagWithInline = comment_parser.Spec & {
line?: Integer;
inlineTags: (JsdocInlineTagNoType & {
line?: Integer;
})[];
};
/**
* Expands on comment-parser's `Block` interface.
*/
type JsdocBlockWithInline = {
description: string;
source: comment_parser.Line[];
problems: comment_parser.Problem[];
tags: JsdocTagWithInline[];
inlineTags: (JsdocInlineTagNoType & {
line?: Integer;
})[];
};
type ESTreeToStringOptions = {
preferRawType?: boolean;
jtppStringificationRules?: (node: estree.Node, options?: any) => string;
};
type CommentHandler = (commentSelector: string, jsdoc: JsdocBlockWithInline) => boolean;
export {
type CommentHandler,
type CommentParserToESTreeOptions,
type CommentParserTokenizer,
type DecoratedNode,
type ESLintOrTSNode,
type ESTreeToStringOptions,
type InlineTag,
type Integer,
JsdocBlock,
type JsdocBlockWithInline,
JsdocDescriptionLine,
JsdocInlineTag,
type JsdocInlineTagNoType,
JsdocTag,
type JsdocTagWithInline,
JsdocTypeLine,
type JtppOptions,
type Token,
commentHandler,
commentParserToESTree,
defaultNoNames,
defaultNoTypes,
estreeToString,
findJSDocComment,
getDecorator,
getFollowingComment,
getJSDocComment,
getNonJsdocComment,
getReducedASTNode,
getTokenizers,
hasSeeWithLink,
type int,
jsdocVisitorKeys,
parseComment,
parseInlineTags,
};

View file

@ -0,0 +1,403 @@
import * as estree from 'estree';
import * as comment_parser from 'comment-parser';
import * as jsdoc_type_pratt_parser from 'jsdoc-type-pratt-parser';
export * from 'jsdoc-type-pratt-parser';
export { visitorKeys as jsdocTypeVisitorKeys } from 'jsdoc-type-pratt-parser';
import * as _typescript_eslint_types from '@typescript-eslint/types';
import * as eslint from 'eslint';
type JsdocTypeLine = {
delimiter: string;
postDelimiter: string;
rawType: string;
initial: string;
type: 'JsdocTypeLine';
};
type JsdocDescriptionLine = {
delimiter: string;
description: string;
postDelimiter: string;
initial: string;
type: 'JsdocDescriptionLine';
};
type JsdocInlineTagNoType = {
format: 'pipe' | 'plain' | 'prefix' | 'space';
namepathOrURL: string;
tag: string;
text: string;
};
type JsdocInlineTag = JsdocInlineTagNoType & {
type: 'JsdocInlineTag';
};
type JsdocTag = {
delimiter: string;
description: string;
descriptionLines: JsdocDescriptionLine[];
initial: string;
inlineTags: JsdocInlineTag[];
name: string;
postDelimiter: string;
postName: string;
postTag: string;
postType: string;
rawType: string;
parsedType: jsdoc_type_pratt_parser.RootResult | null;
tag: string;
type: 'JsdocTag';
typeLines: JsdocTypeLine[];
};
type Integer = number;
type JsdocBlock = {
delimiter: string;
delimiterLineBreak: string;
description: string;
descriptionEndLine?: Integer;
descriptionLines: JsdocDescriptionLine[];
descriptionStartLine?: Integer;
hasPreterminalDescription: 0 | 1;
hasPreterminalTagDescription?: 1;
initial: string;
inlineTags: JsdocInlineTag[];
lastDescriptionLine?: Integer;
endLine: Integer;
lineEnd: string;
postDelimiter: string;
tags: JsdocTag[];
terminal: string;
preterminalLineBreak: string;
type: 'JsdocBlock';
};
type JtppOptions = {
module?: boolean;
strictMode?: boolean;
asyncFunctionBody?: boolean;
classContext?: boolean;
computedPropertyParser?: (text: string, options?: any) => unknown;
};
type CommentParserToESTreeOptions = {
/**
* By default, empty lines are
* compacted; set to 'preserve' to preserve empty comment lines.
*/
spacing?: 'compact' | 'preserve';
throwOnTypeParsingErrors?: boolean;
jsdocTypePrattParserArgs?: JtppOptions;
};
/**
* @typedef {{
* module?: boolean;
* strictMode?: boolean;
* asyncFunctionBody?: boolean;
* classContext?: boolean;
* computedPropertyParser?: (text: string, options?: any) => unknown;
* }} JtppOptions
*/
/**
* @typedef {object} CommentParserToESTreeOptions
* @property {'compact'|'preserve'} [spacing] By default, empty lines are
* compacted; set to 'preserve' to preserve empty comment lines.
* @property {boolean} [throwOnTypeParsingErrors]
* @property {JtppOptions} [jsdocTypePrattParserArgs]
*/
/**
* Converts comment parser AST to ESTree format.
* @param {import('.').JsdocBlockWithInline} jsdoc
* @param {import('jsdoc-type-pratt-parser').ParseMode} mode
* @param {CommentParserToESTreeOptions} [opts]
* @returns {JsdocBlock}
*/
declare function commentParserToESTree(
jsdoc: JsdocBlockWithInline,
mode?: jsdoc_type_pratt_parser.ParseMode,
{ spacing, throwOnTypeParsingErrors, jsdocTypePrattParserArgs }?: CommentParserToESTreeOptions,
): JsdocBlock;
declare namespace jsdocVisitorKeys {
let JsdocBlock: string[];
let JsdocDescriptionLine: any[];
let JsdocTypeLine: any[];
let JsdocTag: string[];
let JsdocInlineTag: any[];
}
/**
* @param {{
* mode: import('jsdoc-type-pratt-parser').ParseMode,
* [key: string]: any
* }} settings
* @param {import('./commentParserToESTree.js').
* CommentParserToESTreeOptions} [commentParserToESTreeOptions]
* @returns {import('.').CommentHandler}
*/
declare function commentHandler(
settings: {
mode: jsdoc_type_pratt_parser.ParseMode;
[key: string]: any;
},
commentParserToESTreeOptions?: CommentParserToESTreeOptions,
): CommentHandler;
/**
* @todo convert for use by escodegen (until may be patched to support
* custom entries?).
* @param {import('./commentParserToESTree').JsdocBlock|
* import('./commentParserToESTree').JsdocDescriptionLine|
* import('./commentParserToESTree').JsdocTypeLine|
* import('./commentParserToESTree').JsdocTag|
* import('./commentParserToESTree').JsdocInlineTag|
* import('jsdoc-type-pratt-parser').RootResult
* } node
* @param {import('.').ESTreeToStringOptions} [opts]
* @throws {Error}
* @returns {string}
*/
declare function estreeToString(
node:
| JsdocBlock
| JsdocDescriptionLine
| JsdocTypeLine
| JsdocTag
| JsdocInlineTag
| jsdoc_type_pratt_parser.RootResult,
opts?: ESTreeToStringOptions,
): string;
type Token =
| eslint.AST.Token
| estree.Comment
| {
type: eslint.AST.TokenType | 'Line' | 'Block' | 'Shebang';
range: [number, number];
value: string;
};
type ESLintOrTSNode = eslint.Rule.Node | _typescript_eslint_types.TSESTree.Node;
type int = number;
type DecoratedNode =
| ESLintOrTSNode
| estree.Comment
| (eslint.Rule.Node & {
declaration?: any;
decorators?: any[];
});
/**
* Reduces the provided node to the appropriate node for evaluating
* JSDoc comment status.
*
* @param {ESLintOrTSNode} node An AST node.
* @param {import('eslint').SourceCode} sourceCode The ESLint SourceCode.
* @returns {ESLintOrTSNode} The AST node that
* can be evaluated for appropriate JSDoc comments.
*/
declare function getReducedASTNode(node: ESLintOrTSNode, sourceCode: eslint.SourceCode): ESLintOrTSNode;
/**
* Retrieves the JSDoc comment for a given node.
*
* @param {import('eslint').SourceCode} sourceCode The ESLint SourceCode
* @param {import('eslint').Rule.Node} node The AST node to get
* the comment for.
* @param {{maxLines: int, minLines: int, [name: string]: any}} settings The
* settings in context
* @param {{checkOverloads?: boolean}} [opts]
* @returns {Token|null} The Block comment
* token containing the JSDoc comment for the given node or
* null if not found.
* @public
*/
declare function getJSDocComment(
sourceCode: eslint.SourceCode,
node: eslint.Rule.Node,
settings: {
maxLines: int;
minLines: int;
[name: string]: any;
},
opts?: {
checkOverloads?: boolean;
},
): Token | null;
/**
* Retrieves the comment preceding a given node.
*
* @param {import('eslint').SourceCode} sourceCode The ESLint SourceCode
* @param {ESLintOrTSNode} node The AST node to get
* the comment for.
* @param {{maxLines: int, minLines: int, [name: string]: any}} settings The
* settings in context
* @returns {Token|null} The Block comment
* token containing the JSDoc comment for the given node or
* null if not found.
* @public
*/
declare function getNonJsdocComment(
sourceCode: eslint.SourceCode,
node: ESLintOrTSNode,
settings: {
maxLines: int;
minLines: int;
[name: string]: any;
},
): Token | null;
/**
* @typedef {(
* ESLintOrTSNode|
* import('estree').Comment|
* import('eslint').Rule.Node & {declaration?: any, decorators?: any[]}
* )} DecoratedNode
*/
/**
* @param {DecoratedNode} node
* @returns {import('@typescript-eslint/types').TSESTree.Decorator|undefined}
*/
declare function getDecorator(node: DecoratedNode): _typescript_eslint_types.TSESTree.Decorator | undefined;
/**
* Checks for the presence of a JSDoc comment for the given node and returns it.
*
* @param {ESLintOrTSNode} astNode The AST node to get
* the comment for.
* @param {import('eslint').SourceCode} sourceCode
* @param {{maxLines: int, minLines: int, [name: string]: any}} settings
* @param {{nonJSDoc?: boolean}} [opts]
* @returns {Token|null} The Block comment token containing the JSDoc comment
* for the given node or null if not found.
*/
declare function findJSDocComment(
astNode: ESLintOrTSNode,
sourceCode: eslint.SourceCode,
settings: {
maxLines: int;
minLines: int;
[name: string]: any;
},
opts?: {
nonJSDoc?: boolean;
},
): Token | null;
/**
* Checks for the presence of a comment following the given node and
* returns it.
*
* This method is experimental.
*
* @param {import('eslint').SourceCode} sourceCode
* @param {ESLintOrTSNode} astNode The AST node to get
* the comment for.
* @returns {Token|null} The comment token containing the comment
* for the given node or null if not found.
*/
declare function getFollowingComment(sourceCode: eslint.SourceCode, astNode: ESLintOrTSNode): Token | null;
declare function hasSeeWithLink(spec: comment_parser.Spec): boolean;
declare const defaultNoTypes: string[];
declare const defaultNoNames: string[];
/**
* Can't import `comment-parser/es6/parser/tokenizers/index.js`,
* so we redefine here.
*/
type CommentParserTokenizer = (spec: comment_parser.Spec) => comment_parser.Spec;
/**
* Can't import `comment-parser/es6/parser/tokenizers/index.js`,
* so we redefine here.
* @typedef {(spec: import('comment-parser').Spec) =>
* import('comment-parser').Spec} CommentParserTokenizer
*/
/**
* @param {object} [cfg]
* @param {string[]} [cfg.noTypes]
* @param {string[]} [cfg.noNames]
* @returns {CommentParserTokenizer[]}
*/
declare function getTokenizers({
noTypes,
noNames,
}?: {
noTypes?: string[];
noNames?: string[];
}): CommentParserTokenizer[];
/**
* Accepts a comment token or complete comment string and converts it into
* `comment-parser` AST.
* @param {string | {value: string}} commentOrNode
* @param {string} [indent] Whitespace
* @returns {import('.').JsdocBlockWithInline}
*/
declare function parseComment(
commentOrNode:
| string
| {
value: string;
},
indent?: string,
): JsdocBlockWithInline;
/**
* Splits the `{@ prefix}` from remaining `Spec.lines[].token.description`
* into the `inlineTags` tokens, and populates `spec.inlineTags`
* @param {import('comment-parser').Block} block
* @returns {import('.').JsdocBlockWithInline}
*/
declare function parseInlineTags(block: comment_parser.Block): JsdocBlockWithInline;
type InlineTag = JsdocInlineTagNoType & {
start: number;
end: number;
};
type JsdocTagWithInline = comment_parser.Spec & {
line?: Integer;
inlineTags: (JsdocInlineTagNoType & {
line?: Integer;
})[];
};
/**
* Expands on comment-parser's `Block` interface.
*/
type JsdocBlockWithInline = {
description: string;
source: comment_parser.Line[];
problems: comment_parser.Problem[];
tags: JsdocTagWithInline[];
inlineTags: (JsdocInlineTagNoType & {
line?: Integer;
})[];
};
type ESTreeToStringOptions = {
preferRawType?: boolean;
jtppStringificationRules?: (node: estree.Node, options?: any) => string;
};
type CommentHandler = (commentSelector: string, jsdoc: JsdocBlockWithInline) => boolean;
export {
type CommentHandler,
type CommentParserToESTreeOptions,
type CommentParserTokenizer,
type DecoratedNode,
type ESLintOrTSNode,
type ESTreeToStringOptions,
type InlineTag,
type Integer,
JsdocBlock,
type JsdocBlockWithInline,
JsdocDescriptionLine,
JsdocInlineTag,
type JsdocInlineTagNoType,
JsdocTag,
type JsdocTagWithInline,
JsdocTypeLine,
type JtppOptions,
type Token,
commentHandler,
commentParserToESTree,
defaultNoNames,
defaultNoTypes,
estreeToString,
findJSDocComment,
getDecorator,
getFollowingComment,
getJSDocComment,
getNonJsdocComment,
getReducedASTNode,
getTokenizers,
hasSeeWithLink,
type int,
jsdocVisitorKeys,
parseComment,
parseInlineTags,
};

View file

@ -0,0 +1,102 @@
{
"name": "@es-joy/jsdoccomment",
"version": "0.78.0",
"author": "Brett Zamir <brettz9@yahoo.com>",
"contributors": [],
"description": "Maintained replacement for ESLint's deprecated SourceCode#getJSDocComment along with other jsdoc utilities",
"license": "MIT",
"keywords": [
"ast",
"comment",
"estree",
"jsdoc",
"parser",
"eslint",
"sourcecode"
],
"type": "module",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./src/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs.cjs"
}
}
},
"browserslist": [
"defaults, not op_mini all"
],
"typedocOptions": {
"dmtLinksService": {
"GitHub": "https://github.com/es-joy/jsdoccomment",
"NPM": "https://www.npmjs.com/package/@es-joy/jsdoccomment"
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/es-joy/jsdoccomment.git"
},
"bugs": {
"url": "https://github.com/es-joy/jsdoccomment/issues"
},
"homepage": "https://github.com/es-joy/jsdoccomment",
"engines": {
"node": ">=20.11.0"
},
"dependencies": {
"@types/estree": "^1.0.8",
"@typescript-eslint/types": "^8.46.4",
"comment-parser": "1.4.1",
"esquery": "^1.6.0",
"jsdoc-type-pratt-parser": "~7.0.0"
},
"devDependencies": {
"@babel/core": "^7.28.5",
"@babel/preset-env": "^7.28.5",
"@brettz9/node-static": "^0.1.1",
"@rollup/plugin-babel": "^6.1.0",
"@types/esquery": "^1.5.4",
"@types/estraverse": "^5.1.7",
"@typescript-eslint/visitor-keys": "^8.46.4",
"@typhonjs-build-test/esm-d-ts": "^0.3.0-next.9",
"@typhonjs-typedoc/typedoc-pkg": "^0.4.0",
"@vitest/coverage-v8": "^4.0.8",
"@vitest/ui": "^4.0.8",
"@webcoder49/code-input": "^2.7.1",
"eslint": "^9.39.1",
"eslint-config-ash-nazg": "39.8.0",
"eslint-plugin-jsdoc": "^61.1.12",
"espree": "^11.0.0",
"estraverse": "^5.3.0",
"prismjs": "^1.30.0",
"rollup": "^4.53.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.4",
"vitest": "^4.0.8"
},
"files": [
"/dist",
"/src",
"CHANGES.md",
"LICENSE-MIT.txt"
],
"scripts": {
"start": "static -p 8070",
"copy": "cp -R node_modules/prismjs/ demo/vendor/prismjs/ && cp -R node_modules/@webcoder49/code-input/ demo/vendor/@webcoder49/code-input/ && cp node_modules/esquery/dist/esquery.esm.js demo/vendor/esquery/dist/esquery.esm.js && cp -R node_modules/jsdoc-type-pratt-parser/dist/esm demo/vendor/jsdoc-type-pratt-parser/dist && cp -R node_modules/comment-parser/es6 demo/vendor/comment-parser",
"build": "npm run copy && rollup -c && npm run types",
"docs": "typedoc-pkg --api-link es",
"eslint": "eslint .",
"lint": "npm run eslint --",
"open": "open ./coverage/index.html",
"test": "npm run lint && npm run build && npm run test-cov",
"test-ui": "vitest --ui --coverage",
"test-cov": "vitest --coverage",
"tsc": "tsc",
"types": "esm-d-ts gen ./src/index.js --output ./dist/index.d.ts --emitCTS"
}
}

View file

@ -0,0 +1,48 @@
import esquery from 'esquery';
import {
visitorKeys as jsdocTypePrattParserVisitorKeys
} from 'jsdoc-type-pratt-parser';
import {
commentParserToESTree, jsdocVisitorKeys
} from './commentParserToESTree.js';
/* eslint-disable jsdoc/reject-any-type -- Arbitrary settings */
/**
* @param {{
* mode: import('jsdoc-type-pratt-parser').ParseMode,
* [key: string]: any
* }} settings
* @param {import('./commentParserToESTree.js').
* CommentParserToESTreeOptions} [commentParserToESTreeOptions]
* @returns {import('.').CommentHandler}
*/
const commentHandler = (settings, commentParserToESTreeOptions) => {
/* eslint-enable jsdoc/reject-any-type -- Arbitrary settings */
/**
* @type {import('.').CommentHandler}
*/
return (commentSelector, jsdoc) => {
const {mode} = settings;
const selector = esquery.parse(commentSelector);
const ast = commentParserToESTree(
jsdoc, mode, commentParserToESTreeOptions
);
const castAst = /** @type {unknown} */ (ast);
return esquery.matches(/** @type {import('estree').Node} */ (
castAst
), selector, undefined, {
visitorKeys: {
...jsdocTypePrattParserVisitorKeys,
...jsdocVisitorKeys
}
});
};
};
export {commentHandler};

View file

@ -0,0 +1,524 @@
import {parse as jsdocTypePrattParse} from 'jsdoc-type-pratt-parser';
/**
* Removes initial and ending brackets from `rawType`
* @param {JsdocTypeLine[]|JsdocTag} container
* @param {boolean} [isArr]
* @returns {void}
*/
const stripEncapsulatingBrackets = (container, isArr) => {
if (isArr) {
const firstItem = /** @type {JsdocTypeLine[]} */ (container)[0];
firstItem.rawType = firstItem.rawType.replace(
/^\{/v, ''
);
const lastItem = /** @type {JsdocTypeLine} */ (
/** @type {JsdocTypeLine[]} */ (
container
).at(-1)
);
lastItem.rawType = lastItem.rawType.replace(/\}$/v, '');
return;
}
/** @type {JsdocTag} */ (container).rawType =
/** @type {JsdocTag} */ (container).rawType.replace(
/^\{/v, ''
).replace(/\}$/v, '');
};
/**
* @typedef {{
* delimiter: string,
* postDelimiter: string,
* rawType: string,
* initial: string,
* type: "JsdocTypeLine"
* }} JsdocTypeLine
*/
/**
* @typedef {{
* delimiter: string,
* description: string,
* postDelimiter: string,
* initial: string,
* type: "JsdocDescriptionLine"
* }} JsdocDescriptionLine
*/
/**
* @typedef {{
* format: 'pipe' | 'plain' | 'prefix' | 'space',
* namepathOrURL: string,
* tag: string,
* text: string,
* }} JsdocInlineTagNoType
*/
/**
* @typedef {JsdocInlineTagNoType & {
* type: "JsdocInlineTag"
* }} JsdocInlineTag
*/
/**
* @typedef {{
* delimiter: string,
* description: string,
* descriptionLines: JsdocDescriptionLine[],
* initial: string,
* inlineTags: JsdocInlineTag[]
* name: string,
* postDelimiter: string,
* postName: string,
* postTag: string,
* postType: string,
* rawType: string,
* parsedType: import('jsdoc-type-pratt-parser').RootResult|null
* tag: string,
* type: "JsdocTag",
* typeLines: JsdocTypeLine[],
* }} JsdocTag
*/
/**
* @typedef {number} Integer
*/
/**
* @typedef {{
* delimiter: string,
* delimiterLineBreak: string,
* description: string,
* descriptionEndLine?: Integer,
* descriptionLines: JsdocDescriptionLine[],
* descriptionStartLine?: Integer,
* hasPreterminalDescription: 0|1,
* hasPreterminalTagDescription?: 1,
* initial: string,
* inlineTags: JsdocInlineTag[]
* lastDescriptionLine?: Integer,
* endLine: Integer,
* lineEnd: string,
* postDelimiter: string,
* tags: JsdocTag[],
* terminal: string,
* preterminalLineBreak: string,
* type: "JsdocBlock",
* }} JsdocBlock
*/
/**
* @param {object} cfg
* @param {string} cfg.text
* @param {string} cfg.tag
* @param {'pipe' | 'plain' | 'prefix' | 'space'} cfg.format
* @param {string} cfg.namepathOrURL
* @returns {JsdocInlineTag}
*/
const inlineTagToAST = ({text, tag, format, namepathOrURL}) => ({
text,
tag,
format,
namepathOrURL,
type: 'JsdocInlineTag'
});
/* eslint-disable jsdoc/reject-any-type -- API */
/**
* @typedef {{
* module?: boolean;
* strictMode?: boolean;
* asyncFunctionBody?: boolean;
* classContext?: boolean;
* computedPropertyParser?: (text: string, options?: any) => unknown;
* }} JtppOptions
*/
/* eslint-enable jsdoc/reject-any-type -- API */
/**
* @typedef {object} CommentParserToESTreeOptions
* @property {'compact'|'preserve'} [spacing] By default, empty lines are
* compacted; set to 'preserve' to preserve empty comment lines.
* @property {boolean} [throwOnTypeParsingErrors]
* @property {JtppOptions} [jsdocTypePrattParserArgs]
*/
/**
* Converts comment parser AST to ESTree format.
* @param {import('.').JsdocBlockWithInline} jsdoc
* @param {import('jsdoc-type-pratt-parser').ParseMode} mode
* @param {CommentParserToESTreeOptions} [opts]
* @returns {JsdocBlock}
*/
const commentParserToESTree = (jsdoc, mode = 'typescript', {
spacing = 'compact',
throwOnTypeParsingErrors = false,
jsdocTypePrattParserArgs
} = {}) => {
/**
* Strips brackets from a tag's `rawType` values and adds `parsedType`
* @param {JsdocTag} lastTag
* @returns {void}
*/
const cleanUpLastTag = (lastTag) => {
// Strip out `}` that encapsulates and is not part of
// the type
stripEncapsulatingBrackets(lastTag);
if (lastTag.typeLines.length) {
stripEncapsulatingBrackets(lastTag.typeLines, true);
}
// Remove single empty line description.
if (lastTag.descriptionLines.length === 1 &&
lastTag.descriptionLines[0].description === '') {
lastTag.descriptionLines.length = 0;
}
// With even a multiline type now in full, add parsing
let parsedType = null;
try {
parsedType = jsdocTypePrattParse(
lastTag.rawType, mode, jsdocTypePrattParserArgs
);
} catch (err) {
// Ignore
if (lastTag.rawType && throwOnTypeParsingErrors) {
/** @type {Error} */ (
err
).message = `Tag @${lastTag.tag} with raw type ` +
`\`${lastTag.rawType}\` had parsing error: ${
/** @type {Error} */ (err).message}`;
throw err;
}
}
lastTag.parsedType = parsedType;
};
const {source, inlineTags: blockInlineTags} = jsdoc;
const {tokens: {
delimiter: delimiterRoot,
lineEnd: lineEndRoot,
postDelimiter: postDelimiterRoot,
start: startRoot,
end: endRoot
}} = source[0];
const endLine = source.length - 1;
/** @type {JsdocBlock} */
const ast = {
delimiter: delimiterRoot,
delimiterLineBreak: '\n',
description: '',
descriptionLines: [],
inlineTags: blockInlineTags.map((t) => inlineTagToAST(t)),
initial: startRoot,
tags: [],
// `terminal` will be overwritten if there are other entries
terminal: endRoot,
preterminalLineBreak: '\n',
hasPreterminalDescription: 0,
endLine,
postDelimiter: postDelimiterRoot,
lineEnd: lineEndRoot,
type: 'JsdocBlock'
};
/**
* @type {JsdocTag[]}
*/
const tags = [];
/** @type {Integer|undefined} */
let lastDescriptionLine;
/** @type {JsdocTag|null} */
let lastTag = null;
// Tracks when first valid tag description line is seen.
let tagDescriptionSeen = false;
let descLineStateOpen = true;
source.forEach((info, idx) => {
const {tokens} = info;
const {
delimiter,
description,
postDelimiter,
start: initial,
tag,
end,
type: rawType
} = tokens;
if (!tag && description && descLineStateOpen) {
if (ast.descriptionStartLine === undefined) {
ast.descriptionStartLine = idx;
}
ast.descriptionEndLine = idx;
}
if (tag || end) {
descLineStateOpen = false;
if (lastDescriptionLine === undefined) {
lastDescriptionLine = idx;
}
// Clean-up with last tag before end or new tag
if (lastTag) {
cleanUpLastTag(lastTag);
}
// Stop the iteration when we reach the end
// but only when there is no tag earlier in the line
// to still process
if (end && !tag) {
ast.terminal = end;
// Check if there are any description lines and if not then this is a
// one line comment block.
const isDelimiterLine = ast.descriptionLines.length === 0 &&
delimiter === '/**';
// Remove delimiter line break for one line comments blocks.
if (isDelimiterLine) {
ast.delimiterLineBreak = '';
}
if (description) {
// Remove terminal line break at end when description is defined.
if (ast.terminal === '*/') {
ast.preterminalLineBreak = '';
}
if (lastTag) {
ast.hasPreterminalTagDescription = 1;
} else {
ast.hasPreterminalDescription = 1;
}
const holder = lastTag || ast;
holder.description += (holder.description ? '\n' : '') + description;
// Do not include `delimiter` / `postDelimiter` for opening
// delimiter line.
holder.descriptionLines.push({
delimiter: isDelimiterLine ? '' : delimiter,
description,
postDelimiter: isDelimiterLine ? '' : postDelimiter,
initial,
type: 'JsdocDescriptionLine'
});
}
return;
}
const {
// eslint-disable-next-line no-unused-vars -- Discarding
end: ed,
delimiter: de,
postDelimiter: pd,
start: init,
...tkns
} = tokens;
if (!tokens.name) {
let i = 1;
while (source[idx + i]) {
const {tokens: {
name,
postName,
postType,
tag: tg
}} = source[idx + i];
if (tg) {
break;
}
if (name) {
tkns.postType = postType;
tkns.name = name;
tkns.postName = postName;
break;
}
i++;
}
}
/**
* @type {JsdocInlineTag[]}
*/
let tagInlineTags = [];
if (tag) {
// Assuming the tags from `source` are in the same order as `jsdoc.tags`
// we can use the `tags` length as index into the parser result tags.
tagInlineTags =
/**
* @type {import('comment-parser').Spec & {
* inlineTags: JsdocInlineTagNoType[]
* }}
*/ (
jsdoc.tags[tags.length]
).inlineTags.map(
(t) => inlineTagToAST(t)
);
}
/** @type {JsdocTag} */
const tagObj = {
...tkns,
initial: endLine ? init : '',
postDelimiter: lastDescriptionLine ? pd : '',
delimiter: lastDescriptionLine ? de : '',
descriptionLines: [],
inlineTags: tagInlineTags,
parsedType: null,
rawType: '',
type: 'JsdocTag',
typeLines: []
};
tagObj.tag = tagObj.tag.replace(/^@/v, '');
lastTag = tagObj;
tagDescriptionSeen = false;
tags.push(tagObj);
}
if (rawType) {
// Will strip rawType brackets after this tag
/** @type {JsdocTag} */ (lastTag).typeLines.push(
/** @type {JsdocTag} */ (lastTag).typeLines.length
? {
delimiter,
postDelimiter,
rawType,
initial,
type: 'JsdocTypeLine'
}
: {
delimiter: '',
postDelimiter: '',
rawType,
initial: '',
type: 'JsdocTypeLine'
}
);
/** @type {JsdocTag} */ (lastTag).rawType += /** @type {JsdocTag} */ (
lastTag
).rawType
? '\n' + rawType
: rawType;
}
// In `compact` mode skip processing if `description` is an empty string
// unless lastTag is being processed.
//
// In `preserve` mode process when `description` is not the `empty string
// or the `delimiter` is not `/**` ensuring empty lines are preserved.
if (((spacing === 'compact' && description) || lastTag) ||
(spacing === 'preserve' && (description || delimiter !== '/**'))) {
const holder = lastTag || ast;
// Check if there are any description lines and if not then this is a
// multi-line comment block with description on 0th line. Treat
// `delimiter` / `postDelimiter` / `initial` as being on a new line.
const isDelimiterLine = holder.descriptionLines.length === 0 &&
delimiter === '/**';
// Remove delimiter line break for one line comments blocks.
if (isDelimiterLine) {
ast.delimiterLineBreak = '';
}
// Track when the first description line is seen to avoid adding empty
// description lines for tag type lines.
tagDescriptionSeen ||= Boolean(lastTag &&
(rawType === '' || rawType?.endsWith('}')));
if (lastTag) {
if (tagDescriptionSeen) {
// The first tag description line is a continuation after type /
// name parsing.
const isFirstDescriptionLine = holder.descriptionLines.length === 0;
// For `compact` spacing must allow through first description line.
if ((spacing === 'compact' &&
(description || isFirstDescriptionLine)) ||
spacing === 'preserve') {
holder.descriptionLines.push({
delimiter: isFirstDescriptionLine ? '' : delimiter,
description,
postDelimiter: isFirstDescriptionLine ? '' : postDelimiter,
initial: isFirstDescriptionLine ? '' : initial,
type: 'JsdocDescriptionLine'
});
}
}
} else {
holder.descriptionLines.push({
delimiter: isDelimiterLine ? '' : delimiter,
description,
postDelimiter: isDelimiterLine ? '' : postDelimiter,
initial: isDelimiterLine ? `` : initial,
type: 'JsdocDescriptionLine'
});
}
if (!tag) {
if (lastTag) {
// For `compact` spacing must filter out any empty description lines
// after the initial `holder.description` has content.
if (tagDescriptionSeen && !(spacing === 'compact' &&
holder.description && description === '')) {
holder.description += !holder.description
? description
: '\n' + description;
}
} else {
holder.description += !holder.description
? description
: '\n' + description;
}
}
}
// Clean-up where last line itself has tag content
if (end && tag) {
ast.terminal = end;
ast.hasPreterminalTagDescription = 1;
// Remove terminal line break at end when tag is defined on last line.
if (ast.terminal === '*/') {
ast.preterminalLineBreak = '';
}
cleanUpLastTag(/** @type {JsdocTag} */ (lastTag));
}
});
ast.lastDescriptionLine = lastDescriptionLine;
ast.tags = tags;
return ast;
};
const jsdocVisitorKeys = {
JsdocBlock: ['descriptionLines', 'tags', 'inlineTags'],
JsdocDescriptionLine: [],
JsdocTypeLine: [],
JsdocTag: ['parsedType', 'typeLines', 'descriptionLines', 'inlineTags'],
JsdocInlineTag: []
};
export {commentParserToESTree, jsdocVisitorKeys};

View file

@ -0,0 +1,182 @@
import {stringify as prattStringify} from 'jsdoc-type-pratt-parser';
/* eslint-disable jsdoc/reject-function-type -- Different functions */
/** @type {Record<string, Function>} */
const stringifiers = {
/* eslint-enable jsdoc/reject-function-type -- Different functions */
JsdocBlock,
/**
* @param {import('./commentParserToESTree').JsdocDescriptionLine} node
* @returns {string}
*/
JsdocDescriptionLine ({
initial, delimiter, postDelimiter, description
}) {
return `${initial}${delimiter}${postDelimiter}${description}`;
},
/**
* @param {import('./commentParserToESTree').JsdocTypeLine} node
* @returns {string}
*/
JsdocTypeLine ({
initial, delimiter, postDelimiter, rawType
}) {
return `${initial}${delimiter}${postDelimiter}${rawType}`;
},
/**
* @param {import('./commentParserToESTree').JsdocInlineTag} node
*/
JsdocInlineTag ({format, namepathOrURL, tag, text}) {
return format === 'pipe'
? `{@${tag} ${namepathOrURL}|${text}}`
: format === 'plain'
? `{@${tag} ${namepathOrURL}}`
: format === 'prefix'
? `[${text}]{@${tag} ${namepathOrURL}}`
// "space"
: `{@${tag} ${namepathOrURL} ${text}}`;
},
JsdocTag
};
/**
* @todo convert for use by escodegen (until may be patched to support
* custom entries?).
* @param {import('./commentParserToESTree').JsdocBlock|
* import('./commentParserToESTree').JsdocDescriptionLine|
* import('./commentParserToESTree').JsdocTypeLine|
* import('./commentParserToESTree').JsdocTag|
* import('./commentParserToESTree').JsdocInlineTag|
* import('jsdoc-type-pratt-parser').RootResult
* } node
* @param {import('.').ESTreeToStringOptions} [opts]
* @throws {Error}
* @returns {string}
*/
function estreeToString (node, opts = {}) {
if (Object.hasOwn(stringifiers, node.type)) {
return stringifiers[
/**
* @type {import('./commentParserToESTree').JsdocBlock|
* import('./commentParserToESTree').JsdocDescriptionLine|
* import('./commentParserToESTree').JsdocTypeLine|
* import('./commentParserToESTree').JsdocTag}
*/
(node).type
](
node,
opts
);
}
// We use raw type instead but it is a key as other apps may wish to traverse
if (node.type.startsWith('JsdocType')) {
return opts.preferRawType
? ''
: `{${prattStringify(
/** @type {import('jsdoc-type-pratt-parser').RootResult} */ (
node
),
opts.jtppStringificationRules
)}}`;
}
throw new Error(`Unhandled node type: ${node.type}`);
}
/**
* @param {import('./commentParserToESTree').JsdocBlock} node
* @param {import('.').ESTreeToStringOptions} opts
* @returns {string}
*/
function JsdocBlock (node, opts) {
const {delimiter, delimiterLineBreak, descriptionLines,
initial, postDelimiter, preterminalLineBreak, tags, terminal} = node;
const terminalPrepend = preterminalLineBreak !== ''
? `${preterminalLineBreak}${initial} `
: '';
let result = `${initial}${delimiter}${postDelimiter}${delimiterLineBreak}`;
for (let i = 0; i < descriptionLines.length; i++) {
result += estreeToString(descriptionLines[i]);
if (i !== descriptionLines.length - 1 || tags.length) {
result += '\n';
}
}
for (let i = 0; i < tags.length; i++) {
result += estreeToString(tags[i], opts);
if (i !== tags.length - 1) {
result += '\n';
}
}
result += `${terminalPrepend}${terminal}`;
return result;
}
/**
* @param {import('./commentParserToESTree').JsdocTag} node
* @param {import('.').ESTreeToStringOptions} opts
* @returns {string}
*/
function JsdocTag (node, opts) {
const {
delimiter, descriptionLines, initial, name, parsedType, postDelimiter,
postName, postTag, postType, tag, typeLines
} = node;
let result = `${initial}${delimiter}${postDelimiter}@${tag}${postTag}`;
// Could do `rawType` but may have been changed; could also do
// `typeLines` but not as likely to be changed
// parsedType
// Comment this out later in favor of `parsedType`
// We can't use raw `typeLines` as first argument has delimiter on it
if (opts.preferRawType || !parsedType) {
if (typeLines.length) {
result += '{';
for (let i = 0; i < typeLines.length; i++) {
result += estreeToString(typeLines[i]);
if (i !== typeLines.length - 1) {
result += '\n';
}
}
result += '}';
}
} else if (parsedType?.type.startsWith('JsdocType')) {
result += `{${prattStringify(
/** @type {import('jsdoc-type-pratt-parser').RootResult} */ (
parsedType
)
)}}`;
}
result += name ? `${postType}${name}${postName}` : postType;
for (let i = 0; i < descriptionLines.length; i++) {
const descriptionLine = descriptionLines[i];
result += estreeToString(descriptionLine);
if (i !== descriptionLines.length - 1) {
result += '\n';
}
}
return result;
}
export {estreeToString};

View file

@ -0,0 +1,57 @@
/**
* @typedef {import('./commentParserToESTree').JsdocInlineTagNoType & {
* start: number,
* end: number,
* }} InlineTag
*/
/**
* @typedef {import('comment-parser').Spec & {
* line?: import('./commentParserToESTree').Integer,
* inlineTags: (import('./commentParserToESTree').JsdocInlineTagNoType & {
* line?: import('./commentParserToESTree').Integer
* })[]
* }} JsdocTagWithInline
*/
/**
* Expands on comment-parser's `Block` interface.
* @typedef {{
* description: string,
* source: import('comment-parser').Line[],
* problems: import('comment-parser').Problem[],
* tags: JsdocTagWithInline[],
* inlineTags: (import('./commentParserToESTree').JsdocInlineTagNoType & {
* line?: import('./commentParserToESTree').Integer
* })[]
* }} JsdocBlockWithInline
*/
/* eslint-disable jsdoc/reject-any-type -- API */
/**
* @typedef {{
* preferRawType?: boolean,
* jtppStringificationRules?: (
* node: import('estree').Node, options?: any
* ) => string
* }} ESTreeToStringOptions
*/
/* eslint-enable jsdoc/reject-any-type -- API */
/**
* @callback CommentHandler
* @param {string} commentSelector
* @param {import('.').JsdocBlockWithInline} jsdoc
* @returns {boolean}
*/
export {visitorKeys as jsdocTypeVisitorKeys} from 'jsdoc-type-pratt-parser';
export * from 'jsdoc-type-pratt-parser';
export * from './commentHandler.js';
export * from './commentParserToESTree.js';
export * from './estreeToString.js';
export * from './jsdoccomment.js';
export * from './parseComment.js';
export * from './parseInlineTags.js';

View file

@ -0,0 +1,559 @@
/* eslint-disable jsdoc/reject-any-type -- Todo */
/**
* Obtained originally from {@link https://github.com/eslint/eslint/blob/master/lib/util/source-code.js#L313}.
*
* @license MIT
*/
/**
* @typedef {import('eslint').AST.Token | import('estree').Comment | {
* type: import('eslint').AST.TokenType|"Line"|"Block"|"Shebang",
* range: [number, number],
* value: string
* }} Token
*/
/**
* @typedef {import('eslint').Rule.Node|
* import('@typescript-eslint/types').TSESTree.Node} ESLintOrTSNode
*/
/**
* @typedef {number} int
*/
/**
* Checks if the given token is a comment token or not.
*
* @param {Token} token - The token to check.
* @returns {boolean} `true` if the token is a comment token.
*/
const isCommentToken = (token) => {
return token.type === 'Line' || token.type === 'Block' ||
token.type === 'Shebang';
};
/**
* @typedef {(
* ESLintOrTSNode|
* import('estree').Comment|
* import('eslint').Rule.Node & {declaration?: any, decorators?: any[]}
* )} DecoratedNode
*/
/**
* @param {DecoratedNode} node
* @returns {import('@typescript-eslint/types').TSESTree.Decorator|undefined}
*/
const getDecorator = (node) => {
// @ts-expect-error -- Loose checking for decorator presence across node kinds
return node?.declaration?.decorators?.[0] ||
// @ts-expect-error -- Loose checking
node?.decorators?.[0] ||
// @ts-expect-error -- Loose checking
node?.parent?.decorators?.[0];
};
/**
* Check to see if it is a ES6 export declaration.
*
* @param {ESLintOrTSNode} astNode An AST node.
* @returns {boolean} whether the given node represents an export declaration.
* @private
*/
const looksLikeExport = function (astNode) {
return astNode.type === 'ExportDefaultDeclaration' ||
astNode.type === 'ExportNamedDeclaration' ||
astNode.type === 'ExportAllDeclaration' ||
astNode.type === 'ExportSpecifier';
};
/**
* @param {ESLintOrTSNode} astNode
* @returns {ESLintOrTSNode}
*/
const getTSFunctionComment = function (astNode) {
const {parent} = astNode;
/* v8 ignore next 3 */
if (!parent) {
return astNode;
}
const grandparent = parent.parent;
/* v8 ignore next 3 */
if (!grandparent) {
return astNode;
}
const greatGrandparent = grandparent.parent;
const greatGreatGrandparent = greatGrandparent && greatGrandparent.parent;
if (/** @type {ESLintOrTSNode} */ (parent).type !== 'TSTypeAnnotation') {
if (
parent.type === 'TSTypeAliasDeclaration' &&
grandparent.type === 'ExportNamedDeclaration'
) {
return grandparent;
/* v8 ignore next 3 */
}
return astNode;
}
switch (/** @type {ESLintOrTSNode} */ (grandparent).type) {
// @ts-expect-error -- For `ClassProperty`.
case 'PropertyDefinition': case 'ClassProperty':
case 'TSDeclareFunction':
case 'TSMethodSignature':
case 'TSPropertySignature':
return grandparent;
case 'ArrowFunctionExpression':
/* v8 ignore next 3 */
if (!greatGrandparent) {
return astNode;
}
if (
greatGrandparent.type === 'VariableDeclarator'
// && greatGreatGrandparent.parent.type === 'VariableDeclaration'
) {
/* v8 ignore next 3 */
if (!greatGreatGrandparent || !greatGreatGrandparent.parent) {
return astNode;
}
return greatGreatGrandparent.parent;
/* v8 ignore next 2 */
}
return astNode;
case 'FunctionExpression':
/* v8 ignore next 3 */
if (!greatGreatGrandparent) {
return astNode;
}
if (greatGrandparent.type === 'MethodDefinition') {
return greatGrandparent;
}
// Fallthrough
default:
/* v8 ignore next 3 */
if (grandparent.type !== 'Identifier') {
return astNode;
}
}
/* v8 ignore next 3 */
if (!greatGreatGrandparent) {
return astNode;
}
switch (greatGrandparent.type) {
case 'ArrowFunctionExpression':
if (
greatGreatGrandparent.type === 'VariableDeclarator' &&
greatGreatGrandparent.parent.type === 'VariableDeclaration'
) {
return greatGreatGrandparent.parent;
}
return astNode;
case 'FunctionDeclaration':
return greatGrandparent;
case 'VariableDeclarator':
if (greatGreatGrandparent.type === 'VariableDeclaration') {
return greatGreatGrandparent;
}
/* v8 ignore next 2 */
// Fallthrough
default:
/* v8 ignore next 3 */
return astNode;
}
};
const invokedExpression = new Set(
['CallExpression', 'OptionalCallExpression', 'NewExpression']
);
const allowableCommentNode = new Set([
'AssignmentPattern',
'VariableDeclaration',
'ExpressionStatement',
'MethodDefinition',
'Property',
'ObjectProperty',
'ClassProperty',
'PropertyDefinition',
'ExportDefaultDeclaration',
'ReturnStatement'
]);
/**
* Reduces the provided node to the appropriate node for evaluating
* JSDoc comment status.
*
* @param {ESLintOrTSNode} node An AST node.
* @param {import('eslint').SourceCode} sourceCode The ESLint SourceCode.
* @returns {ESLintOrTSNode} The AST node that
* can be evaluated for appropriate JSDoc comments.
*/
const getReducedASTNode = function (node, sourceCode) {
let {parent} = node;
switch (/** @type {ESLintOrTSNode} */ (node).type) {
case 'TSFunctionType':
return getTSFunctionComment(node);
case 'TSInterfaceDeclaration':
case 'TSTypeAliasDeclaration':
case 'TSEnumDeclaration':
case 'ClassDeclaration':
case 'FunctionDeclaration':
/* v8 ignore next 3 */
if (!parent) {
return node;
}
return looksLikeExport(parent) ? parent : node;
case 'TSDeclareFunction':
case 'ClassExpression':
case 'ObjectExpression':
case 'ArrowFunctionExpression':
case 'TSEmptyBodyFunctionExpression':
case 'FunctionExpression':
/* v8 ignore next 3 */
if (!parent) {
return node;
}
if (
!invokedExpression.has(parent.type)
) {
/**
* @type {ESLintOrTSNode|Token|null}
*/
let token = node;
do {
token = sourceCode.getTokenBefore(
/** @type {import('eslint').Rule.Node|import('eslint').AST.Token} */ (
token
),
{includeComments: true}
);
} while (token && token.type === 'Punctuator' && token.value === '(');
if (token && token.type === 'Block') {
return node;
}
if (sourceCode.getCommentsBefore(
/** @type {import('eslint').Rule.Node} */
(node)
).length) {
return node;
}
while (
!sourceCode.getCommentsBefore(
/** @type {import('eslint').Rule.Node} */
(parent)
).length &&
!(/Function/v).test(parent.type) &&
!allowableCommentNode.has(parent.type)
) {
({parent} = parent);
if (!parent) {
break;
}
}
if (parent && parent.type !== 'FunctionDeclaration' &&
parent.type !== 'Program'
) {
if (parent.parent && parent.parent.type === 'ExportNamedDeclaration') {
return parent.parent;
}
return parent;
}
}
return node;
default:
return node;
}
};
/**
* Checks for the presence of a JSDoc comment for the given node and returns it.
*
* @param {ESLintOrTSNode} astNode The AST node to get
* the comment for.
* @param {import('eslint').SourceCode} sourceCode
* @param {{maxLines: int, minLines: int, [name: string]: any}} settings
* @param {{nonJSDoc?: boolean}} [opts]
* @returns {Token|null} The Block comment token containing the JSDoc comment
* for the given node or null if not found.
*/
const findJSDocComment = (astNode, sourceCode, settings, opts = {}) => {
const {nonJSDoc} = opts;
const {minLines, maxLines} = settings;
/** @type {ESLintOrTSNode|import('estree').Comment} */
let currentNode = astNode;
let tokenBefore = null;
let parenthesisToken = null;
while (currentNode) {
const decorator = getDecorator(currentNode);
if (decorator) {
const dec = /** @type {unknown} */ (decorator);
currentNode = /** @type {import('eslint').Rule.Node} */ (dec);
}
tokenBefore = sourceCode.getTokenBefore(
/** @type {import('eslint').Rule.Node} */
(currentNode),
{includeComments: true}
);
if (
tokenBefore && tokenBefore.type === 'Punctuator' &&
tokenBefore.value === '('
) {
parenthesisToken = tokenBefore;
[tokenBefore] = sourceCode.getTokensBefore(
/** @type {import('eslint').Rule.Node} */
(currentNode),
{
count: 2,
includeComments: true
}
);
}
if (!tokenBefore || !isCommentToken(tokenBefore)) {
return null;
}
if (!nonJSDoc && tokenBefore.type === 'Line') {
currentNode = tokenBefore;
continue;
}
break;
}
/* v8 ignore next 3 */
if (!tokenBefore || !currentNode.loc || !tokenBefore.loc) {
return null;
}
if (
(
(nonJSDoc && (tokenBefore.type !== 'Block' ||
!(/^\*\s/v).test(tokenBefore.value))) ||
(!nonJSDoc && tokenBefore.type === 'Block' &&
(/^\*\s/v).test(tokenBefore.value))
) &&
currentNode.loc.start.line - (
/** @type {import('eslint').AST.Token} */
(parenthesisToken ?? tokenBefore)
).loc.end.line >= minLines &&
currentNode.loc.start.line - (
/** @type {import('eslint').AST.Token} */
(parenthesisToken ?? tokenBefore)
).loc.end.line <= maxLines
) {
return tokenBefore;
}
return null;
};
/**
* Retrieves the JSDoc comment for a given node.
*
* @param {import('eslint').SourceCode} sourceCode The ESLint SourceCode
* @param {import('eslint').Rule.Node} node The AST node to get
* the comment for.
* @param {{maxLines: int, minLines: int, [name: string]: any}} settings The
* settings in context
* @param {{checkOverloads?: boolean}} [opts]
* @returns {Token|null} The Block comment
* token containing the JSDoc comment for the given node or
* null if not found.
* @public
*/
const getJSDocComment = function (sourceCode, node, settings, opts = {}) {
const reducedNode = getReducedASTNode(node, sourceCode);
const comment = findJSDocComment(reducedNode, sourceCode, settings);
if (!comment &&
opts.checkOverloads &&
(
reducedNode.parent?.type === 'Program' ||
reducedNode.parent?.type === 'ExportNamedDeclaration'
)
) {
let functionName;
if (reducedNode.type === 'TSDeclareFunction' ||
reducedNode.type === 'FunctionDeclaration') {
functionName = reducedNode.id?.name;
} else if (reducedNode.type === 'ExportNamedDeclaration' &&
(reducedNode.declaration?.type === 'FunctionDeclaration' ||
// @ts-expect-error Should be ok
reducedNode.declaration?.type === 'TSDeclareFunction')
) {
functionName = reducedNode.declaration.id.name;
} else {
return null;
}
/**
* @type {import('estree').Program}
*/
let programNode;
/**
* @type {ESLintOrTSNode}
*/
let childNode;
if (reducedNode.parent?.type === 'Program') {
programNode = reducedNode.parent;
childNode = reducedNode;
} else if (reducedNode.parent?.parent.type === 'Program') {
programNode = reducedNode.parent.parent;
childNode = reducedNode.parent;
/* v8 ignore next 3 */
} else {
throw new Error('unexpected TS guard condition');
}
// @ts-expect-error Should be ok
const idx = programNode.body.indexOf(childNode);
const prevSibling = /** @type {import('eslint').AST.Program} */ (
programNode
).body[idx - 1];
if (
// @ts-expect-error Should be ok
(prevSibling?.type === 'TSDeclareFunction' &&
// @ts-expect-error Should be ok
functionName === prevSibling.id.name) ||
(prevSibling?.type === 'ExportNamedDeclaration' &&
// @ts-expect-error Should be ok
prevSibling.declaration?.type === 'TSDeclareFunction' &&
// @ts-expect-error Should be ok
prevSibling.declaration?.id?.name === functionName)
) {
// @ts-expect-error Should be ok
return getJSDocComment(sourceCode, prevSibling, settings, opts);
}
}
return comment;
};
/**
* Retrieves the comment preceding a given node.
*
* @param {import('eslint').SourceCode} sourceCode The ESLint SourceCode
* @param {ESLintOrTSNode} node The AST node to get
* the comment for.
* @param {{maxLines: int, minLines: int, [name: string]: any}} settings The
* settings in context
* @returns {Token|null} The Block comment
* token containing the JSDoc comment for the given node or
* null if not found.
* @public
*/
const getNonJsdocComment = function (sourceCode, node, settings) {
const reducedNode = getReducedASTNode(node, sourceCode);
return findJSDocComment(reducedNode, sourceCode, settings, {
nonJSDoc: true
});
};
/**
* @param {ESLintOrTSNode|import('eslint').AST.Token|
* import('estree').Comment
* } nodeA The AST node or token to compare
* @param {ESLintOrTSNode|import('eslint').AST.Token|
* import('estree').Comment} nodeB The
* AST node or token to compare
*/
const compareLocEndToStart = (nodeA, nodeB) => {
/* v8 ignore next */
return (nodeA.loc?.end.line ?? 0) === (nodeB.loc?.start.line ?? 0);
};
/**
* Checks for the presence of a comment following the given node and
* returns it.
*
* This method is experimental.
*
* @param {import('eslint').SourceCode} sourceCode
* @param {ESLintOrTSNode} astNode The AST node to get
* the comment for.
* @returns {Token|null} The comment token containing the comment
* for the given node or null if not found.
*/
const getFollowingComment = function (sourceCode, astNode) {
/**
* @param {ESLintOrTSNode} node The
* AST node to get the comment for.
*/
const getTokensAfterIgnoringSemis = (node) => {
let tokenAfter = sourceCode.getTokenAfter(
/** @type {import('eslint').Rule.Node} */
(node),
{includeComments: true}
);
while (
tokenAfter && tokenAfter.type === 'Punctuator' &&
// tokenAfter.value === ')' // Don't apparently need to ignore
tokenAfter.value === ';'
) {
[tokenAfter] = sourceCode.getTokensAfter(tokenAfter, {
includeComments: true
});
}
return tokenAfter;
};
/**
* @param {ESLintOrTSNode} node The
* AST node to get the comment for.
*/
const tokenAfterIgnoringSemis = (node) => {
const tokenAfter = getTokensAfterIgnoringSemis(node);
return (
tokenAfter &&
isCommentToken(tokenAfter) &&
compareLocEndToStart(node, tokenAfter)
)
? tokenAfter
: null;
};
let tokenAfter = tokenAfterIgnoringSemis(astNode);
if (!tokenAfter) {
switch (astNode.type) {
case 'FunctionDeclaration':
tokenAfter = tokenAfterIgnoringSemis(
/** @type {ESLintOrTSNode} */
(astNode.body)
);
break;
case 'ExpressionStatement':
tokenAfter = tokenAfterIgnoringSemis(
/** @type {ESLintOrTSNode} */
(astNode.expression)
);
break;
/* v8 ignore next 3 */
default:
break;
}
}
return tokenAfter;
};
export {
getReducedASTNode, getJSDocComment, getNonJsdocComment,
getDecorator, findJSDocComment, getFollowingComment
};

View file

@ -0,0 +1,190 @@
/* eslint-disable prefer-named-capture-group -- Temporary */
import {
parse as commentParser,
tokenizers
} from 'comment-parser';
import {parseInlineTags} from './parseInlineTags.js';
const {
name: nameTokenizer,
tag: tagTokenizer,
type: typeTokenizer,
description: descriptionTokenizer
} = tokenizers;
/**
* @param {import('comment-parser').Spec} spec
* @returns {boolean}
*/
export const hasSeeWithLink = (spec) => {
return spec.tag === 'see' && (/\{@link.+?\}/v).test(spec.source[0].source);
};
export const defaultNoTypes = [
'default', 'defaultvalue', 'description', 'example',
'file', 'fileoverview', 'license',
'overview', 'see', 'summary'
];
export const defaultNoNames = [
'access', 'author',
'default', 'defaultvalue',
'description',
'example', 'exception', 'file', 'fileoverview',
'kind',
'license', 'overview',
'return', 'returns',
'since', 'summary',
'throws',
'version', 'variation'
];
const optionalBrackets = /^\[(?<name>[^=]*)=[^\]]*\]/v;
const preserveTypeTokenizer = typeTokenizer('preserve');
const preserveDescriptionTokenizer = descriptionTokenizer('preserve');
const plainNameTokenizer = nameTokenizer();
/**
* Can't import `comment-parser/es6/parser/tokenizers/index.js`,
* so we redefine here.
* @typedef {(spec: import('comment-parser').Spec) =>
* import('comment-parser').Spec} CommentParserTokenizer
*/
/**
* @param {object} [cfg]
* @param {string[]} [cfg.noTypes]
* @param {string[]} [cfg.noNames]
* @returns {CommentParserTokenizer[]}
*/
const getTokenizers = ({
noTypes = defaultNoTypes,
noNames = defaultNoNames
} = {}) => {
// trim
return [
// Tag
tagTokenizer(),
/**
* Type tokenizer.
* @param {import('comment-parser').Spec} spec
* @returns {import('comment-parser').Spec}
*/
(spec) => {
if (noTypes.includes(spec.tag)) {
return spec;
}
return preserveTypeTokenizer(spec);
},
/**
* Name tokenizer.
* @param {import('comment-parser').Spec} spec
* @returns {import('comment-parser').Spec}
*/
(spec) => {
if (spec.tag === 'template') {
// const preWS = spec.postTag;
const remainder = spec.source[0].tokens.description;
let pos;
if (remainder.startsWith('[') && remainder.includes(']')) {
const endingBracketPos = remainder.lastIndexOf(']');
pos = remainder.slice(endingBracketPos).search(/(?<![\s,])\s/v);
if (pos > -1) { // Add offset to starting point if space found
pos += endingBracketPos;
}
} else {
pos = remainder.search(/(?<![\s,])\s/v);
}
const name = pos === -1 ? remainder : remainder.slice(0, pos);
const extra = remainder.slice(pos);
let postName = '', description = '', lineEnd = '';
if (pos > -1) {
[, postName, description, lineEnd] = /** @type {RegExpMatchArray} */ (
extra.match(/(\s*)([^\r]*)(\r)?/v)
);
}
spec.optional = optionalBrackets.test(name);
// name = /** @type {string} */ (
// /** @type {RegExpMatchArray} */ (
// name.match(optionalBrackets)
// )?.groups?.name
// );
spec.name = name;
const {tokens} = spec.source[0];
tokens.name = name;
tokens.postName = postName;
tokens.description = description;
tokens.lineEnd = lineEnd || '';
return spec;
}
if (noNames.includes(spec.tag) || hasSeeWithLink(spec)) {
return spec;
}
return plainNameTokenizer(spec);
},
/**
* Description tokenizer.
* @param {import('comment-parser').Spec} spec
* @returns {import('comment-parser').Spec}
*/
(spec) => {
return preserveDescriptionTokenizer(spec);
}
];
};
/**
* Accepts a comment token or complete comment string and converts it into
* `comment-parser` AST.
* @param {string | {value: string}} commentOrNode
* @param {string} [indent] Whitespace
* @returns {import('.').JsdocBlockWithInline}
*/
const parseComment = (commentOrNode, indent = '') => {
let result;
switch (typeof commentOrNode) {
case 'string':
// Preserve JSDoc block start/end indentation.
result = commentParser(`${indent}${commentOrNode}`, {
// @see https://github.com/yavorskiy/comment-parser/issues/21
tokenizers: getTokenizers()
});
break;
case 'object':
if (commentOrNode === null) {
throw new TypeError(`'commentOrNode' is not a string or object.`);
}
// Preserve JSDoc block start/end indentation.
result = commentParser(`${indent}/*${commentOrNode.value}*/`, {
// @see https://github.com/yavorskiy/comment-parser/issues/21
tokenizers: getTokenizers()
});
break;
default:
throw new TypeError(`'commentOrNode' is not a string or object.`);
}
if (!result.length) {
throw new Error('There were no results for comment parsing');
}
const [block] = result;
return parseInlineTags(block);
};
export {getTokenizers, parseComment};

View file

@ -0,0 +1,108 @@
/**
* @param {RegExpMatchArray & {
* indices: {
* groups: {
* [key: string]: [number, number]
* }
* }
* groups: {[key: string]: string}
* }} match An inline tag regexp match.
* @returns {'pipe' | 'plain' | 'prefix' | 'space'}
*/
function determineFormat (match) {
const {separator, text} = match.groups;
const [, textEnd] = match.indices.groups.text;
const [tagStart] = match.indices.groups.tag;
if (!text) {
return 'plain';
} else if (separator === '|') {
return 'pipe';
} else if (textEnd < tagStart) {
return 'prefix';
}
return 'space';
}
/**
* Extracts inline tags from a description.
* @param {string} description
* @returns {import('.').InlineTag[]} Array of inline tags from the description.
*/
function parseDescription (description) {
/** @type {import('.').InlineTag[]} */
const result = [];
// This could have been expressed in a single pattern,
// but having two avoids a potentially exponential time regex.
const prefixedTextPattern = /(?:\[(?<text>[^\]]+)\])\{@(?<tag>[^\}\s]+)\s?(?<namepathOrURL>[^\}\s\|]*)\}/gvd;
// The pattern used to match for text after tag uses a negative lookbehind
// on the ']' char to avoid matching the prefixed case too.
const suffixedAfterPattern = /(?<!\])\{@(?<tag>[^\}\s]+)\s?(?<namepathOrURL>[^\}\s\|]*)\s*(?<separator>[\s\|])?\s*(?<text>[^\}]*)\}/gvd;
const matches = [
...description.matchAll(prefixedTextPattern),
...description.matchAll(suffixedAfterPattern)
];
for (const mtch of matches) {
const match = /**
* @type {RegExpMatchArray & {
* indices: {
* groups: {
* [key: string]: [number, number]
* }
* }
* groups: {[key: string]: string}
* }}
*/ (
mtch
);
const {tag, namepathOrURL, text} = match.groups;
const [start, end] = match.indices[0];
const format = determineFormat(match);
result.push({
tag,
namepathOrURL,
text,
format,
start,
end
});
}
return result;
}
/**
* Splits the `{@ prefix}` from remaining `Spec.lines[].token.description`
* into the `inlineTags` tokens, and populates `spec.inlineTags`
* @param {import('comment-parser').Block} block
* @returns {import('.').JsdocBlockWithInline}
*/
export function parseInlineTags (block) {
const inlineTags =
/**
* @type {(import('./commentParserToESTree').JsdocInlineTagNoType & {
* line?: import('./commentParserToESTree').Integer
* })[]}
*/ (
parseDescription(block.description)
);
/** @type {import('.').JsdocBlockWithInline} */ (
block
).inlineTags = inlineTags;
for (const tag of block.tags) {
/**
* @type {import('.').JsdocTagWithInline}
*/ (tag).inlineTags = parseDescription(tag.description);
}
return (
/**
* @type {import('.').JsdocBlockWithInline}
*/ (block)
);
}

View file

@ -0,0 +1,13 @@
/**
* @param {string} str
* @returns {string}
*/
const toCamelCase = (str) => {
return str.toLowerCase().replaceAll(/^[a-z]/gv, (init) => {
return init.toUpperCase();
}).replaceAll(/_(?<wordInit>[a-z])/gv, (_, n1, o, s, {wordInit}) => {
return wordInit.toUpperCase();
});
};
export {toCamelCase};

View file

@ -0,0 +1 @@
function e(e,t,n){throw new Error(n?`No known conditions for "${t}" specifier in "${e}" package`:`Missing "${t}" specifier in "${e}" package`)}function t(e){let t=new Set(["default"]);e.unsafe||t.add(e.require?"require":"import"),e.unsafe||t.add(e.browser?"browser":"node");for(const n of e.conditions||[])n.startsWith("!")?t.delete(n.slice(1)):t.add(n);return t}function n(n,f,o,l){let u,g,c=i(n,o),a=t(l||{}),p=f[c];if(void 0===p){let e,t,n,r;for(r in f)t&&r.length<t.length||("/"===r[r.length-1]&&c.startsWith(r)?(g=c.substring(r.length),t=r):r.length>1&&(n=r.indexOf("*",1),~n&&(e=RegExp("^"+r.substring(0,n)+"(.*)"+r.substring(1+n)+"$").exec(c),e&&e[1]&&(g=e[1],t=r))));p=f[t]}return p||e(n,c),u=s(p,a),u||e(n,c,1),g&&r(u,g),u}function r(e,t){let n,r=0,i=e.length,s=/[*]/g,f=/[/]$/;for(;r<i;r++)e[r]=s.test(n=e[r])?n.replace(s,t):f.test(n)?n+t:n}function i(e,t,n){if(e===t||"."===t)return".";let r=e+"/",i=r.length,s=t.slice(0,i)===r,f=s?t.slice(i):t;return"#"===f[0]?f:s||!n?"./"===f.slice(0,2)?f:"./"+f:f}function s(e,t,n){if(e){if("string"==typeof e)return n&&n.add(e),[e];let r,i;if(Array.isArray(e)){for(i=n||new Set,r=0;r<e.length;r++)s(e[r],t,i);if(!n&&i.size)return[...i]}else for(r in e)if(t.has(r))return s(e[r],t,n)}}function f(n,s,f,l){let u,g,c=i(n,f),a=t(l||{}),p=s[c];if(void 0===p){let e,t,n,r;for(r in s)t&&r.length<t.length||("/"===r[r.length-1]&&c.startsWith(r)?(g=c.substring(r.length),t=r):r.length>1&&(n=r.indexOf("*",1),~n&&(e=RegExp("^"+r.substring(0,n)+"(.*)"+r.substring(1+n)+"$").exec(c),e&&e[1]&&(g=e[1],t=r))));p=s[t]}if(p||e(n,c),u=o(p,a),u)return g&&r(u,g),u}function o(e,t,n,r){if(null==e)return;if("string"==typeof e)return r?(n&&n.add(e),[e]):void 0;let i,s;if(Array.isArray(e)){for(s=n||new Set,i=0;i<e.length;i++)o(e[i],t,s,r);return!n&&s.size?[...s]:void 0}if("types"in e){const r=o(e.types,t,n,!0);if(r&&r.length)return r}else if("typings"in e){const r=o(e.typings,t,n,!0);if(r&&r.length)return r}for(i in e)if("types"!==i&&"types"!==i&&t.has(i)){const s=o(e[i],t,n,r);if(s&&s.length)return s}}function l(e,t={}){let n,r=0,s=t.browser,f=t.fields||["module","main"],o="string"==typeof s;for(s&&!f.includes("browser")&&(f.unshift("browser"),o&&(s=i(e.name,s,!0)));r<f.length;r++)if(n=e[f[r]]){if("string"==typeof n);else{if("object"!=typeof n||"browser"!=f[r])continue;if(o&&(n=n[s],null==n))return s}return"string"==typeof n?"./"+n.replace(/^\.?\//,""):n}}function u(e,t,r){let i,s=e.exports;if(s){if("string"==typeof s)s={".":s};else for(i in s){"."!==i[0]&&(s={".":s});break}return n(e.name,s,t||".",r)}}function g(e,t,r){if(e.imports)return n(e.name,e.imports,t,r)}function c(e,t,n){return"#"===(t=i(e.name,t||"."))[0]?g(e,t,n):u(e,t,n)}function a(e,t,n){let r,i=e.exports;if(i){if("string"==typeof i)i={".":i};else for(r in i){"."!==r[0]&&(i={".":i});break}return f(e.name,i,t||".",n)}}exports.exports=u;exports.imports=g;exports.legacy=l;exports.resolve=c;exports.types=a;

View file

@ -0,0 +1 @@
function e(e,t,n){throw new Error(n?`No known conditions for "${t}" specifier in "${e}" package`:`Missing "${t}" specifier in "${e}" package`)}function t(e){let t=new Set(["default"]);e.unsafe||t.add(e.require?"require":"import"),e.unsafe||t.add(e.browser?"browser":"node");for(const n of e.conditions||[])n.startsWith("!")?t.delete(n.slice(1)):t.add(n);return t}function n(n,f,o,l){let u,g,c=i(n,o),a=t(l||{}),p=f[c];if(void 0===p){let e,t,n,r;for(r in f)t&&r.length<t.length||("/"===r[r.length-1]&&c.startsWith(r)?(g=c.substring(r.length),t=r):r.length>1&&(n=r.indexOf("*",1),~n&&(e=RegExp("^"+r.substring(0,n)+"(.*)"+r.substring(1+n)+"$").exec(c),e&&e[1]&&(g=e[1],t=r))));p=f[t]}return p||e(n,c),u=s(p,a),u||e(n,c,1),g&&r(u,g),u}function r(e,t){let n,r=0,i=e.length,s=/[*]/g,f=/[/]$/;for(;r<i;r++)e[r]=s.test(n=e[r])?n.replace(s,t):f.test(n)?n+t:n}function i(e,t,n){if(e===t||"."===t)return".";let r=e+"/",i=r.length,s=t.slice(0,i)===r,f=s?t.slice(i):t;return"#"===f[0]?f:s||!n?"./"===f.slice(0,2)?f:"./"+f:f}function s(e,t,n){if(e){if("string"==typeof e)return n&&n.add(e),[e];let r,i;if(Array.isArray(e)){for(i=n||new Set,r=0;r<e.length;r++)s(e[r],t,i);if(!n&&i.size)return[...i]}else for(r in e)if(t.has(r))return s(e[r],t,n)}}function f(n,s,f,l){let u,g,c=i(n,f),a=t(l||{}),p=s[c];if(void 0===p){let e,t,n,r;for(r in s)t&&r.length<t.length||("/"===r[r.length-1]&&c.startsWith(r)?(g=c.substring(r.length),t=r):r.length>1&&(n=r.indexOf("*",1),~n&&(e=RegExp("^"+r.substring(0,n)+"(.*)"+r.substring(1+n)+"$").exec(c),e&&e[1]&&(g=e[1],t=r))));p=s[t]}if(p||e(n,c),u=o(p,a),u)return g&&r(u,g),u}function o(e,t,n,r){if(null==e)return;if("string"==typeof e)return r?(n&&n.add(e),[e]):void 0;let i,s;if(Array.isArray(e)){for(s=n||new Set,i=0;i<e.length;i++)o(e[i],t,s,r);return!n&&s.size?[...s]:void 0}if("types"in e){const r=o(e.types,t,n,!0);if(r&&r.length)return r}else if("typings"in e){const r=o(e.typings,t,n,!0);if(r&&r.length)return r}for(i in e)if("types"!==i&&"types"!==i&&t.has(i)){const s=o(e[i],t,n,r);if(s&&s.length)return s}}function l(e,t={}){let n,r=0,s=t.browser,f=t.fields||["module","main"],o="string"==typeof s;for(s&&!f.includes("browser")&&(f.unshift("browser"),o&&(s=i(e.name,s,!0)));r<f.length;r++)if(n=e[f[r]]){if("string"==typeof n);else{if("object"!=typeof n||"browser"!=f[r])continue;if(o&&(n=n[s],null==n))return s}return"string"==typeof n?"./"+n.replace(/^\.?\//,""):n}}function u(e,t,r){let i,s=e.exports;if(s){if("string"==typeof s)s={".":s};else for(i in s){"."!==i[0]&&(s={".":s});break}return n(e.name,s,t||".",r)}}function g(e,t,r){if(e.imports)return n(e.name,e.imports,t,r)}function c(e,t,n){return"#"===(t=i(e.name,t||"."))[0]?g(e,t,n):u(e,t,n)}function a(e,t,n){let r,i=e.exports;if(i){if("string"==typeof i)i={".":i};else for(r in i){"."!==r[0]&&(i={".":i});break}return f(e.name,i,t||".",n)}}export{u as exports,g as imports,l as legacy,c as resolve,a as types};

View file

@ -0,0 +1,106 @@
export type Options = {
/**
* When true, adds the "browser" conditions.
* Otherwise the "node" condition is enabled.
* @default false
*/
browser?: boolean;
/**
* Any custom conditions to match.
* @note Array order does not matter. Priority is determined by the key-order of conditions defined within a package's imports/exports mapping.
* @default []
*/
conditions?: readonly string[];
/**
* When true, adds the "require" condition.
* Otherwise the "import" condition is enabled.
* @default false
*/
require?: boolean;
/**
* Prevents "require", "import", "browser", and/or "node" conditions from being added automatically.
* When enabled, only `options.conditions` are added alongside the "default" condition.
* @important Enabling this deviates from Node.js default behavior.
* @default false
*/
unsafe?: boolean;
}
export function resolve<T=Package>(pkg: T, entry?: string, options?: Options): Imports.Output | Exports.Output | void;
export function imports<T=Package>(pkg: T, entry?: string, options?: Options): Imports.Output | void;
export function exports<T=Package>(pkg: T, target: string, options?: Options): Exports.Output | void;
export function legacy<T=Package>(pkg: T, options: { browser: true, fields?: readonly string[] }): Browser | void;
export function legacy<T=Package>(pkg: T, options: { browser: string, fields?: readonly string[] }): string | false | void;
export function legacy<T=Package>(pkg: T, options: { browser: false, fields?: readonly string[] }): string | void;
export function legacy<T=Package>(pkg: T, options?: {
browser?: boolean | string;
fields?: readonly string[];
}): Browser | string;
/**
* Resolve only `types` and `typings` entries within exports, even when nested under
* condition branches like "import"/"require". Returns undefined if not found.
*/
export function types<T=Package>(pkg: T, target?: string, options?: Options): Exports.Output | void;
// ---
/**
* A resolve condition
* @example "node", "default", "production"
*/
export type Condition = string;
/** An internal file path */
export type Path = `./${string}`;
export type Imports = {
[entry: Imports.Entry]: Imports.Value;
}
export namespace Imports {
export type Entry = `#${string}`;
type External = string;
/** strings are dependency names OR internal paths */
export type Value = External | Path | null | {
[c: Condition]: Value;
} | Value[];
export type Output = Array<External|Path>;
}
export type Exports = Path | {
[path: Exports.Entry]: Exports.Value;
[cond: Condition]: Exports.Value;
}
export namespace Exports {
/** Allows "." and "./{name}" */
export type Entry = `.${string}`;
/** strings must be internal paths */
export type Value = Path | null | {
[c: Condition]: Value;
} | Value[];
export type Output = Path[];
}
export type Package = {
name: string;
version?: string;
module?: string;
main?: string;
imports?: Imports;
exports?: Exports;
browser?: Browser;
[key: string]: any;
}
export type Browser = string[] | string | {
[file: Path | string]: string | false;
}

View file

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) Luke Edwards <luke.edwards05@gmail.com> (lukeed.com),
Brett Zamir
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,53 @@
{
"version": "1.2.0",
"name": "@es-joy/resolve.exports",
"repository": "es-joy/resolve.exports",
"description": "A tiny (952b), correct, general-purpose, and configurable \"exports\" and \"imports\" resolver without file-system reliance",
"module": "dist/index.mjs",
"main": "dist/index.js",
"types": "index.d.ts",
"license": "MIT",
"author": {
"name": "Luke Edwards",
"email": "luke.edwards05@gmail.com",
"url": "https://lukeed.com"
},
"contributors": [
"Brett Zamir"
],
"engines": {
"node": ">=10"
},
"files": [
"*.d.ts",
"dist"
],
"exports": {
".": {
"types": "./index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
},
"./package.json": "./package.json"
},
"keywords": [
"esm",
"exports",
"esmodules",
"fields",
"modules",
"resolution",
"resolve"
],
"devDependencies": {
"bundt": "next",
"tsm": "2.3.0",
"typescript": "5.9.3",
"uvu": "0.5.6"
},
"scripts": {
"build": "bundt -m",
"types": "tsc --noEmit",
"test": "uvu -r tsm test"
}
}

View file

@ -0,0 +1,497 @@
# @es-joy/resolve.exports [![CI](https://github.com/es-joy/resolve.exports/workflows/CI/badge.svg)](https://github.com/es-joy/resolve.exports/actions) [![licenses](https://licenses.dev/b/npm/resolve.exports)](https://licenses.dev/npm/resolve.exports) [![codecov](https://codecov.io/gh/es-joy/resolve.exports/branch/master/graph/badge.svg?token=4P7d4Omw2h)](https://codecov.io/gh/es-joy/resolve.exports)
> A tiny (952b), correct, general-purpose, and configurable `"exports"` and `"imports"` resolver without file-system reliance
***Why?***
Hopefully, this module may serve as a reference point (and/or be used directly) so that the varying tools and bundlers within the ecosystem can share a common approach with one another **as well as** with the native Node.js implementation.
With the push for ESM, we must be _very_ careful and avoid fragmentation. If we, as a community, begin propagating different _dialects_ of the resolution algorithm, then we're headed for deep trouble. It will make supporting (and using) `"exports"` nearly impossible, which may force its abandonment and along with it, its benefits.
Let's have nice things.
## Install
```sh
$ npm install @es-joy/resolve.exports
```
## Usage
> Please see [`/test/`](/test) for examples.
```js
import * as resolve from '@es-joy/resolve.exports';
// package.json contents
const pkg = {
"name": "foobar",
"module": "dist/module.mjs",
"main": "dist/require.js",
"imports": {
"#hash": {
"import": {
"browser": "./hash/web.mjs",
"node": "./hash/node.mjs",
},
"default": "./hash/detect.js"
}
},
"exports": {
".": {
"import": "./dist/module.mjs",
"require": "./dist/require.js"
},
"./lite": {
"worker": {
"browser": "./lite/worker.browser.js",
"node": "./lite/worker.node.js"
},
"import": "./lite/module.mjs",
"require": "./lite/require.js"
}
}
};
// ---
// Exports
// ---
// entry: "foobar" === "." === default
// conditions: ["default", "import", "node"]
resolve.exports(pkg);
resolve.exports(pkg, '.');
resolve.exports(pkg, 'foobar');
//=> ["./dist/module.mjs"]
// entry: "foobar/lite" === "./lite"
// conditions: ["default", "import", "node"]
resolve.exports(pkg, 'foobar/lite');
resolve.exports(pkg, './lite');
//=> ["./lite/module.mjs"]
// Enable `require` condition
// conditions: ["default", "require", "node"]
resolve.exports(pkg, 'foobar', { require: true }); //=> ["./dist/require.js"]
resolve.exports(pkg, './lite', { require: true }); //=> ["./lite/require.js"]
// Throws "Missing <entry> specifier in <name> package" Error
resolve.exports(pkg, 'foobar/hello');
resolve.exports(pkg, './hello/world');
// Add custom condition(s)
// conditions: ["default", "worker", "import", "node"]
resolve.exports(pkg, 'foobar/lite', {
conditions: ['worker']
}); //=> ["./lite/worker.node.js"]
// Toggle "browser" condition
// conditions: ["default", "worker", "import", "browser"]
resolve.exports(pkg, 'foobar/lite', {
conditions: ['worker'],
browser: true
}); //=> ["./lite/worker.browser.js"]
// ---
// Types-only resolution
// ---
// Find only `types` entries (or its synonym `typings`), even when nested under
// conditions like `import`/`require`
// Defaults to following the active branch (`import` by default, `require` when set)
resolve.types({
name: 'pkg',
exports: {
'.': {
import: { types: './index.d.ts', default: './index.mjs' },
require: { types: './index.cjs.d.ts', default: './index.cjs' }
}
}
});
//=> ["./index.d.ts"]
resolve.types({
name: 'pkg',
exports: {
'.': {
import: { types: './index.d.ts', default: './index.mjs' },
require: { types: './index.cjs.d.ts', default: './index.cjs' }
}
}
}, '.', { require: true });
//=> ["./index.cjs.d.ts"]
// Disable non-"default" condition activate
// NOTE: breaks from Node.js default behavior
// conditions: ["default", "custom"]
resolve.exports(pkg, 'foobar/lite', {
conditions: ['custom'],
unsafe: true,
});
//=> Error: No known conditions for "./lite" specifier in "foobar" package
// ---
// Imports
// ---
// conditions: ["default", "import", "node"]
resolve.imports(pkg, '#hash');
resolve.imports(pkg, 'foobar/#hash');
//=> ["./hash/node.mjs"]
// conditions: ["default", "import", "browser"]
resolve.imports(pkg, '#hash', { browser: true });
resolve.imports(pkg, 'foobar/#hash');
//=> ["./hash/web.mjs"]
// conditions: ["default"]
resolve.imports(pkg, '#hash', { unsafe: true });
resolve.imports(pkg, 'foobar/#hash');
//=> ["./hash/detect.mjs"]
resolve.imports(pkg, '#hello/world');
resolve.imports(pkg, 'foobar/#hello/world');
//=> Error: Missing "#hello/world" specifier in "foobar" package
// ---
// Legacy
// ---
// prefer "module" > "main" (default)
resolve.legacy(pkg); //=> "dist/module.mjs"
// customize fields order
resolve.legacy(pkg, {
fields: ['main', 'module']
}); //=> "dist/require.js"
```
## API
The [`resolve()`](#resolvepkg-entry-options), [`exports()`](#exportspkg-entry-options), and [`imports()`](#importspkg-target-options) functions share similar API signatures:
```ts
export function resolve(pkg: Package, entry?: string, options?: Options): string[] | undefined;
export function exports(pkg: Package, entry?: string, options?: Options): string[] | undefined;
export function imports(pkg: Package, target: string, options?: Options): string[] | undefined;
export function types(pkg: Package, entry?: string, options?: Options): string[] | undefined;
// ^ not optional!
```
All three:
* accept a `package.json` file's contents as a JSON object
* accept a target/entry identifier
* may accept an [Options](#options) object
* return `string[]`, `string`, or `undefined`
The only difference is that `imports()` must accept a target identifier as there can be no inferred default.
See below for further API descriptions.
> **Note:** There is also a [Legacy Resolver API](#legacy-resolver)
---
### resolve(pkg, entry?, options?)
Returns: `string[]` or `undefined`
A convenience helper which automatically reroutes to [`exports()`](#exportspkg-entry-options) or [`imports()`](#importspkg-target-options) depending on the `entry` value.
When unspecified, `entry` defaults to the `"."` identifier, which means that `exports()` will be invoked.
```js
import * as r from '@es-joy/resolve.exports';
let pkg = {
name: 'foobar',
// ...
};
r.resolve(pkg);
//~> r.exports(pkg, '.');
r.resolve(pkg, 'foobar');
//~> r.exports(pkg, '.');
r.resolve(pkg, 'foobar/subpath');
//~> r.exports(pkg, './subpath');
r.resolve(pkg, '#hash/md5');
//~> r.imports(pkg, '#hash/md5');
r.resolve(pkg, 'foobar/#hash/md5');
//~> r.imports(pkg, '#hash/md5');
```
### exports(pkg, entry?, options?)
Returns: `string[]` or `undefined`
Traverse the `"exports"` within the contents of a `package.json` file. <br>
If the contents _does not_ contain an `"exports"` map, then `undefined` will be returned.
Successful resolutions will always result in a `string` or `string[]` value. This will be the value of the resolved mapping itself which means that the output is a relative file path.
This function may throw an Error if:
* the requested `entry` cannot be resolved (aka, not defined in the `"exports"` map)
* an `entry` _is_ defined but no known conditions were matched (see [`options.conditions`](#optionsconditions))
#### pkg
Type: `object` <br>
Required: `true`
The `package.json` contents.
#### entry
Type: `string` <br>
Required: `false` <br>
Default: `.` (aka, root)
The desired target entry, or the original `import` path.
When `entry` _is not_ a relative path (aka, does not start with `'.'`), then `entry` is given the `'./'` prefix.
When `entry` begins with the package name (determined via the `pkg.name` value), then `entry` is truncated and made relative.
When `entry` is already relative, it is accepted as is.
***Examples***
Assume we have a module named "foobar" and whose `pkg` contains `"name": "foobar"`.
| `entry` value | treated as | reason |
|-|-|-|
| `null` / `undefined` | `'.'` | default |
| `'.'` | `'.'` | value was relative |
| `'foobar'` | `'.'` | value was `pkg.name` |
| `'foobar/lite'` | `'./lite'` | value had `pkg.name` prefix |
| `'./lite'` | `'./lite'` | value was relative |
| `'lite'` | `'./lite'` | value was not relative & did not have `pkg.name` prefix |
### imports(pkg, target, options?)
Returns: `string[]` or `undefined`
Traverse the `"imports"` within the contents of a `package.json` file. <br>
If the contents _does not_ contain an `"imports"` map, then `undefined` will be returned.
Successful resolutions will always result in a `string` or `string[]` value. This will be the value of the resolved mapping itself which means that the output is a relative file path.
This function may throw an Error if:
* the requested `target` cannot be resolved (aka, not defined in the `"imports"` map)
* an `target` _is_ defined but no known conditions were matched (see [`options.conditions`](#optionsconditions))
#### pkg
Type: `object` <br>
Required: `true`
The `package.json` contents.
#### target
Type: `string` <br>
Required: `true`
The target import identifier; for example, `#hash` or `#hash/md5`.
Import specifiers _must_ begin with the `#` character, as required by the resolution specification. However, if `target` begins with the package name (determined by the `pkg.name` value), then `resolve.exports` will trim it from the `target` identifier. For example, `"foobar/#hash/md5"` will be treated as `"#hash/md5"` for the `"foobar"` package.
## Options
The [`resolve()`](#resolvepkg-entry-options), [`imports()`](#importspkg-target-options), and [`exports()`](#exportspkg-entry-options) functions share these options. All properties are optional and you are not required to pass an `options` argument.
Collectively, the `options` are used to assemble a list of [conditions](https://nodejs.org/docs/latest-v18.x/api/packages.html#conditional-exports) that should be activated while resolving your target(s).
> **Note:** Although the Node.js documentation primarily showcases conditions alongside `"exports"` usage, they also apply to `"imports"` maps too. _([example](https://nodejs.org/docs/latest-v18.x/api/packages.html#subpath-imports))_
#### options.require
Type: `boolean` <br>
Default: `false`
When truthy, the `"require"` field is added to the list of allowed/known conditions. <br>
Otherwise the `"import"` field is added instead.
#### options.browser
Type: `boolean` <br>
Default: `false`
When truthy, the `"browser"` field is added to the list of allowed/known conditions. <br>
Otherwise the `"node"` field is added instead.
#### options.conditions
Type: `string[]` <br>
Default: `[]`
A list of additional/custom conditions that should be accepted when seen.
> **Important:** The order specified within `options.conditions` does not matter. <br>The matching order/priority is **always** determined by the `"exports"` map's key order.
For example, you may choose to accept a `"production"` condition in certain environments. Given the following `pkg` content:
```js
const pkg = {
// package.json ...
"exports": {
"worker": "./$worker.js",
"require": "./$require.js",
"production": "./$production.js",
"import": "./$import.mjs",
}
};
resolve.exports(pkg, '.');
// Conditions: ["default", "import", "node"]
//=> ["./$import.mjs"]
resolve.exports(pkg, '.', {
conditions: ['production']
});
// Conditions: ["default", "production", "import", "node"]
//=> ["./$production.js"]
resolve.exports(pkg, '.', {
conditions: ['production'],
require: true,
});
// Conditions: ["default", "production", "require", "node"]
//=> ["./$require.js"]
resolve.exports(pkg, '.', {
conditions: ['production', 'worker'],
require: true,
});
// Conditions: ["default", "production", "worker", "require", "node"]
//=> ["./$worker.js"]
resolve.exports(pkg, '.', {
conditions: ['production', 'worker']
});
// Conditions: ["default", "production", "worker", "import", "node"]
//=> ["./$worker.js"]
```
If you want to remove a condition, you can use the `!` prefix. For example, you only need `"types"`:
```js
resolve.exports(pkg, '.', {
conditions: ['!default', '!import', '!node', 'types']
});
// Conditions: ["types"]
```
#### options.unsafe
Type: `boolean` <br>
Default: `false`
> **Important:** You probably do not want this option! <br>It will break out of Node's default resolution conditions.
When enabled, this option will ignore **all other options** except [`options.conditions`](#optionsconditions). This is because, when enabled, `options.unsafe` **does not** assume or provide any default conditions except the `"default"` condition.
```js
resolve.exports(pkg, '.');
//=> Conditions: ["default", "import", "node"]
resolve.exports(pkg, '.', { unsafe: true });
//=> Conditions: ["default"]
resolve.exports(pkg, '.', { unsafe: true, require: true, browser: true });
//=> Conditions: ["default"]
```
In other words, this means that trying to use `options.require` or `options.browser` alongside `options.unsafe` will have no effect. In order to enable these conditions, you must provide them manually into the `options.conditions` list:
```js
resolve.exports(pkg, '.', {
unsafe: true,
conditions: ["require"]
});
//=> Conditions: ["default", "require"]
resolve.exports(pkg, '.', {
unsafe: true,
conditions: ["browser", "require", "custom123"]
});
//=> Conditions: ["default", "browser", "require", "custom123"]
```
## Legacy Resolver
Also included is a "legacy" method for resolving non-`"exports"` package fields. This may be used as a fallback method when for when no `"exports"` mapping is defined. In other words, it's completely optional (and tree-shakeable).
### legacy(pkg, options?)
Returns: `string` or `undefined`
You may customize the field priority via [`options.fields`](#optionsfields).
When a field is found, its value is returned _as written_. <br>
When no fields were found, `undefined` is returned. If you wish to mimic Node.js behavior, you can assume this means `'index.js'` but this module does not make that assumption for you.
#### options.browser
Type: `boolean` or `string` <br>
Default: `false`
When truthy, ensures that the `'browser'` field is part of the acceptable `fields` list.
> **Important:** If your custom [`options.fields`](#optionsfields) value includes `'browser'`, then _your_ order is respected. <br>Otherwise, when truthy, `options.browser` will move `'browser'` to the front of the list, making it the top priority.
When `true` and `"browser"` is an object, then `legacy()` will return the the entire `"browser"` object.
You may also pass a string value, which will be treated as an import/file path. When this is the case and `"browser"` is an object, then `legacy()` may return:
* `false` if the package author decided a file should be ignored; or
* your `options.browser` string value but made relative, if not already
> See the [`"browser" field specification](https://github.com/defunctzombie/package-browser-field-spec) for more information.
#### options.fields
Type: `string[]` <br>
Default: `['module', 'main']`
A list of fields to accept. The order of the array determines the priority/importance of each field, with the most important fields at the beginning of the list.
By default, the `legacy()` method will accept any `"module"` and/or "main" fields if they are defined. However, if both fields are defined, then "module" will be returned.
```js
import { legacy } from '@es-joy/resolve.exports';
// package.json
const pkg = {
"name": "...",
"worker": "worker.js",
"module": "module.mjs",
"browser": "browser.js",
"main": "main.js",
};
legacy(pkg);
// fields = [module, main]
//=> "module.mjs"
legacy(pkg, { browser: true });
// fields = [browser, module, main]
//=> "browser.mjs"
legacy(pkg, {
fields: ['missing', 'worker', 'module', 'main']
});
// fields = [missing, worker, module, main]
//=> "worker.js"
legacy(pkg, {
fields: ['missing', 'worker', 'module', 'main'],
browser: true,
});
// fields = [browser, missing, worker, module, main]
//=> "browser.js"
legacy(pkg, {
fields: ['module', 'browser', 'main'],
browser: true,
});
// fields = [module, browser, main]
//=> "module.mjs"
```
## License
MIT