diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml
new file mode 100644
index 0000000..9b18a96
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug-report.yml
@@ -0,0 +1,81 @@
+name: "\U0001F41E Report a problem"
+description: "Report something that isn't working the way you expected."
+title: "Bug: (fill in)"
+labels:
+ - bug
+ - "repro:needed"
+body:
+ - type: markdown
+ attributes:
+ value: By opening an issue, you agree to abide by the [OpenJS Foundation Code of Conduct](https://eslint.org/conduct).
+ - type: textarea
+ attributes:
+ label: Environment
+ description: |
+ Please tell us about how you're running ESLint (Run `npx eslint --env-info`.)
+ value: |
+ ESLint version:
+ @eslint/json version:
+ Node version:
+ npm version:
+ Operating System:
+ validations:
+ required: true
+ - type: dropdown
+ attributes:
+ label: Which language are you using?
+ description: |
+ Just tell us which language mode you're using.
+ options:
+ - json
+ - jsonc
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: What did you do?
+ description: |
+ Please include a *minimal* reproduction case.
+ value: |
+
+ Configuration
+
+ ```
+
+ ```
+
+
+ ```js
+
+ ```
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: What did you expect to happen?
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: What actually happened?
+ description: |
+ Please copy-paste the actual ESLint output.
+ validations:
+ required: true
+ - type: input
+ attributes:
+ label: Link to Minimal Reproducible Example
+ description: "Link to a [StackBlitz](https://stackblitz.com), or GitHub repo with a minimal reproduction of the problem. **A minimal reproduction is required** so that others can help debug your issue. If a report is vague (e.g. just a generic error message) and has no reproduction, it may be auto-closed."
+ placeholder: "https://stackblitz.com/abcd1234"
+ validations:
+ required: true
+ - type: checkboxes
+ attributes:
+ label: Participation
+ options:
+ - label: I am willing to submit a pull request for this issue.
+ required: false
+ - type: textarea
+ attributes:
+ label: Additional comments
+ description: Is there anything else that's important for the team to know?
diff --git a/.github/ISSUE_TEMPLATE/change.yml b/.github/ISSUE_TEMPLATE/change.yml
new file mode 100644
index 0000000..a685993
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/change.yml
@@ -0,0 +1,51 @@
+name: "\U0001F680 Request a change (not rule-related)"
+description: "Request a change that is not a bug fix, rule change, or new rule"
+title: "Change Request: (fill in)"
+labels:
+ - enhancement
+ - core
+body:
+ - type: markdown
+ attributes:
+ value: By opening an issue, you agree to abide by the [OpenJS Foundation Code of Conduct](https://eslint.org/conduct).
+ - type: textarea
+ attributes:
+ label: Environment
+ description: |
+ Please tell us about how you're running ESLint (Run `npx eslint --env-info`.)
+ value: |
+ ESLint version:
+ @eslint/json version:
+ Node version:
+ npm version:
+ Operating System:
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: What problem do you want to solve?
+ description: |
+ Please explain your use case in as much detail as possible.
+ placeholder: |
+ The JSON plugin currently...
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: What do you think is the correct solution?
+ description: |
+ Please explain how you'd like to change the JSON plugin to address the problem.
+ placeholder: |
+ I'd like the JSON plugin to...
+ validations:
+ required: true
+ - type: checkboxes
+ attributes:
+ label: Participation
+ options:
+ - label: I am willing to submit a pull request for this change.
+ required: false
+ - type: textarea
+ attributes:
+ label: Additional comments
+ description: Is there anything else that's important for the team to know?
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..c2ff969
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
+blank_issues_enabled: false
+contact_links:
+ - name: 🗣 Ask a Question, Discuss
+ url: https://github.com/eslint/json/discussions
+ about: Get help using this plugin
+ - name: Discord Server
+ url: https://eslint.org/chat
+ about: Talk with the team
diff --git a/.github/ISSUE_TEMPLATE/docs.yml b/.github/ISSUE_TEMPLATE/docs.yml
new file mode 100644
index 0000000..a8a3bac
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/docs.yml
@@ -0,0 +1,46 @@
+name: "\U0001F4DD Docs"
+description: "Request an improvement to documentation"
+title: "Docs: (fill in)"
+labels:
+ - documentation
+body:
+ - type: markdown
+ attributes:
+ value: By opening an issue, you agree to abide by the [OpenJS Foundation Code of Conduct](https://eslint.org/conduct).
+ - type: textarea
+ attributes:
+ label: Docs page(s)
+ description: |
+ What page(s) are you suggesting be changed or created?
+ placeholder: |
+ e.g. https://eslint.org/docs/latest/use/getting-started
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: What documentation issue do you want to solve?
+ description: |
+ Please explain your issue in as much detail as possible.
+ placeholder: |
+ The docs currently...
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: What do you think is the correct solution?
+ description: |
+ Please explain how you'd like to change the docs to address the problem.
+ placeholder: |
+ I'd like the docs to...
+ validations:
+ required: true
+ - type: checkboxes
+ attributes:
+ label: Participation
+ options:
+ - label: I am willing to submit a pull request for this change.
+ required: false
+ - type: textarea
+ attributes:
+ label: Additional comments
+ description: Is there anything else that's important for the team to know?
diff --git a/.github/ISSUE_TEMPLATE/new-rule.yml b/.github/ISSUE_TEMPLATE/new-rule.yml
new file mode 100644
index 0000000..6f6c941
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/new-rule.yml
@@ -0,0 +1,41 @@
+name: "\U0001F680 Propose a new rule"
+description: "Propose a new rule to be added to the plugin"
+title: "New Rule: (fill in)"
+labels:
+ - rule
+ - feature
+body:
+ - type: markdown
+ attributes:
+ value: By opening an issue, you agree to abide by the [OpenJS Foundation Code of Conduct](https://eslint.org/conduct).
+ - type: input
+ attributes:
+ label: Rule details
+ description: What should the new rule do?
+ validations:
+ required: true
+ - type: dropdown
+ attributes:
+ label: What type of rule is this?
+ options:
+ - Warns about a potential problem
+ - Suggests an alternate way of doing something
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: Example code
+ description: Please provide some example code that this rule will warn about. This field will render as JSON.
+ render: jsonc
+ validations:
+ required: true
+ - type: checkboxes
+ attributes:
+ label: Participation
+ options:
+ - label: I am willing to submit a pull request to implement this rule.
+ required: false
+ - type: textarea
+ attributes:
+ label: Additional comments
+ description: Is there anything else that's important for the team to know?
diff --git a/.github/ISSUE_TEMPLATE/rule-change.yml b/.github/ISSUE_TEMPLATE/rule-change.yml
new file mode 100644
index 0000000..b2c27f0
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/rule-change.yml
@@ -0,0 +1,61 @@
+name: "\U0001F4DD Request a rule change"
+description: "Request a change to an existing rule"
+title: "Rule Change: (fill in)"
+labels:
+ - enhancement
+ - rule
+body:
+ - type: markdown
+ attributes:
+ value: By opening an issue, you agree to abide by the [OpenJS Foundation Code of Conduct](https://eslint.org/conduct).
+ - type: input
+ attributes:
+ label: What rule do you want to change?
+ validations:
+ required: true
+ - type: dropdown
+ attributes:
+ label: What change do you want to make?
+ options:
+ - Generate more warnings
+ - Generate fewer warnings
+ - Implement autofix
+ - Implement suggestions
+ validations:
+ required: true
+ - type: dropdown
+ attributes:
+ label: How do you think the change should be implemented?
+ options:
+ - A new option
+ - A new default behavior
+ - Other
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: Example code
+ description: Please provide some example code that this change will affect. This field will render as JSON.
+ render: jsonc
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: What does the rule currently do for this code?
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: What will the rule do after it's changed?
+ validations:
+ required: true
+ - type: checkboxes
+ attributes:
+ label: Participation
+ options:
+ - label: I am willing to submit a pull request to implement this change.
+ required: false
+ - type: textarea
+ attributes:
+ label: Additional comments
+ description: Is there anything else that's important for the team to know?
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..1f44735
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,34 @@
+
+
+#### Prerequisites checklist
+
+- [ ] I have read the [contributing guidelines](https://github.com/eslint/eslint/blob/HEAD/CONTRIBUTING.md).
+
+
+
+
+
+#### What is the purpose of this pull request?
+
+#### What changes did you make? (Give an overview)
+
+#### Related Issues
+
+
+
+#### Is there anything you'd like reviewers to focus on?
+
+
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..b44a53c
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,45 @@
+name: CI
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+jobs:
+ lint:
+ name: Lint
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "lts/*"
+ - name: Install dependencies
+ run: npm install
+ - name: Build commonjs
+ run: npm run build
+ - name: Lint files
+ run: npm run lint
+ test:
+ name: Test
+ strategy:
+ matrix:
+ os: [ubuntu-latest]
+ node: [22.x, 21.x, 20.x, 18.x, "18.18.0"]
+ include:
+ - os: windows-latest
+ node: "lts/*"
+ - os: macOS-latest
+ node: "lts/*"
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node }}
+ - name: Install dependencies
+ run: npm install
+ - name: Run tests
+ run: npm run test
diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml
new file mode 100644
index 0000000..04a463f
--- /dev/null
+++ b/.github/workflows/release-please.yml
@@ -0,0 +1,43 @@
+on:
+ push:
+ branches:
+ - main
+name: release-please
+jobs:
+ release-please:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ pull-requests: write
+ id-token: write
+ steps:
+ - uses: google-github-actions/release-please-action@v4
+ id: release
+ with:
+ release-type: node
+ - uses: actions/checkout@v4
+ if: ${{ steps.release.outputs.release_created }}
+ - uses: actions/setup-node@v4
+ with:
+ node-version: lts/*
+ registry-url: https://registry.npmjs.org
+ if: ${{ steps.release.outputs.release_created }}
+ - run: |
+ npm install
+ npm run build
+ npm publish --provenance
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ if: ${{ steps.release.outputs.release_created }}
+ - run: 'npx @humanwhocodes/tweet "eslint/json ${{ steps.release.outputs.tag_name }} has been released: ${{ steps.release.outputs.html_url }}"'
+ if: ${{ steps.release.outputs.release_created }}
+ env:
+ TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }}
+ TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }}
+ TWITTER_ACCESS_TOKEN_KEY: ${{ secrets.TWITTER_ACCESS_TOKEN_KEY }}
+ TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
+ - run: 'npx @humanwhocodes/toot "eslint/json ${{ steps.release.outputs.tag_name }} has been released: ${{ steps.release.outputs.html_url }}"'
+ if: ${{ steps.release.outputs.release_created }}
+ env:
+ MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }}
+ MASTODON_HOST: ${{ secrets.MASTODON_HOST }}
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
new file mode 100644
index 0000000..b985ff6
--- /dev/null
+++ b/.release-please-manifest.json
@@ -0,0 +1,3 @@
+{
+ ".": "0.0.1"
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..e69de29
diff --git a/README.md b/README.md
index dc68213..27a8701 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,8 @@
This package contains a plugin that allows you to natively lint JSON and JSONC files using ESLint.
+**Important:** This plugin requires ESLint v9.6.0 or higher and you must be using the [new configuration system](https://eslint.org/docs/latest/use/configure/configuration-files).
+
## Installation
For Node.js and compatible runtimes:
@@ -28,8 +30,8 @@ deno add @eslint/json
This package exports two different languages:
-* `"json/json"` is for regular JSON files
-* `"json/jsonc"` is for JSON files that support comments (JSON-C)
+- `"json/json"` is for regular JSON files
+- `"json/jsonc"` is for JSON files that support comments (JSON-C)
Depending on which types of JSON files you'd like to lint, you can set up your `eslint.config.js` file to include just the files you'd like. Here's an example that lints both JSON and JSON-C files:
@@ -39,8 +41,8 @@ import json from "@eslint/json";
export default [
{
plugins: {
- json
- }
+ json,
+ },
},
// lint JSON files
@@ -48,8 +50,8 @@ export default [
files: ["**/*.json"],
language: "json/json",
rules: {
- "no-duplicate-keys": "error"
- }
+ "json/no-duplicate-keys": "error",
+ },
},
// lint JSON-C files
@@ -57,17 +59,41 @@ export default [
files: ["**/*.jsonc"],
language: "json/jsonc",
rules: {
- "no-duplicate-keys": "error"
- }
+ "json/no-duplicate-keys": "error",
+ },
},
+];
+```
+## Recommended Configuration
+
+To use the recommended configuration for this plugin, specify your matching `files` and then use the `json.configs.recommended` object, like this:
+
+```js
+import json from "@eslint/json";
+
+export default [
+
+ // lint JSON files
+ {
+ files: ["**/*.json"],
+ language: "json/json",
+ ...json.configs.recommended,
+ },
+
+ // lint JSON-C files
+ {
+ files: ["**/*.jsonc"],
+ language: "json/jsonc",
+ ...json.configs.recommended,
+ },
];
```
## Rules
-* `no-duplicate-keys` - warns when there are two keys in an object with the same text.
-* `no-empty-keys` - warns when there is a key in an object that is an empty string or contains only whitespace
+- `no-duplicate-keys` - warns when there are two keys in an object with the same text.
+- `no-empty-keys` - warns when there is a key in an object that is an empty string or contains only whitespace
## License
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..25b797d
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,70 @@
+/**
+ * @fileoverview ESLint configuration file.
+ * @author Nicholas C. Zakas
+ */
+
+//-----------------------------------------------------------------------------
+// Imports
+//-----------------------------------------------------------------------------
+
+import eslintConfigESLint from "eslint-config-eslint";
+import json from "./src/index.js";
+
+//-----------------------------------------------------------------------------
+// Helpers
+//-----------------------------------------------------------------------------
+
+const eslintPluginJSDoc = eslintConfigESLint.find(
+ config => config.plugins?.jsdoc,
+).plugins.jsdoc;
+
+//-----------------------------------------------------------------------------
+// Configuration
+//-----------------------------------------------------------------------------
+
+export default [
+ {
+ ignores: ["**/tests/fixtures/", "**/dist/"],
+ },
+
+ ...eslintConfigESLint.map(config => ({
+ files: ["**/*.js"],
+ ...config,
+ })),
+ {
+ files: ["**/*.json"],
+ ignores: ["**/package-lock.json"],
+ language: "json/json",
+ ...json.configs.recommended,
+ },
+ {
+ files: ["**/*.js"],
+ rules: {
+ // disable rules we don't want to use from eslint-config-eslint
+ "no-undefined": "off",
+
+ // TODO: re-enable eslint-plugin-jsdoc rules
+ ...Object.fromEntries(
+ Object.keys(eslintPluginJSDoc.rules).map(name => [
+ `jsdoc/${name}`,
+ "off",
+ ]),
+ ),
+ },
+ },
+ {
+ files: ["**/tests/**"],
+ languageOptions: {
+ globals: {
+ describe: "readonly",
+ xdescribe: "readonly",
+ it: "readonly",
+ xit: "readonly",
+ beforeEach: "readonly",
+ afterEach: "readonly",
+ before: "readonly",
+ after: "readonly",
+ },
+ },
+ },
+];
diff --git a/jsr.json b/jsr.json
new file mode 100644
index 0000000..05905a6
--- /dev/null
+++ b/jsr.json
@@ -0,0 +1,14 @@
+{
+ "name": "@eslint/json",
+ "version": "0.0.1",
+ "exports": "./dist/esm/index.js",
+ "publish": {
+ "include": [
+ "dist/esm/index.js",
+ "dist/esm/index.d.ts",
+ "README.md",
+ "jsr.json",
+ "LICENSE"
+ ]
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..4fb9a3c
--- /dev/null
+++ b/package.json
@@ -0,0 +1,84 @@
+{
+ "name": "@eslint/json",
+ "version": "0.0.1",
+ "description": "JSON linting plugin for ESLint",
+ "author": "Nicholas C. Zakas",
+ "type": "module",
+ "main": "dist/esm/index.js",
+ "types": "dist/esm/index.d.ts",
+ "exports": {
+ "require": {
+ "types": "./dist/cjs/index.d.cts",
+ "default": "./dist/cjs/index.cjs"
+ },
+ "import": {
+ "types": "./dist/esm/index.d.ts",
+ "default": "./dist/esm/index.js"
+ }
+ },
+ "files": [
+ "dist"
+ ],
+ "publishConfig": {
+ "access": "public"
+ },
+ "gitHooks": {
+ "pre-commit": "lint-staged"
+ },
+ "lint-staged": {
+ "*.js": [
+ "eslint --fix",
+ "prettier --write"
+ ],
+ "!(*.js)": "prettier --write --ignore-unknown"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/eslint/json.git"
+ },
+ "bugs": {
+ "url": "https://github.com/eslint/json/issues"
+ },
+ "homepage": "https://github.com/eslint/json#readme",
+ "scripts": {
+ "build:dedupe-types": "node tools/dedupe-types.js dist/cjs/index.cjs dist/esm/index.js",
+ "build:cts": "node -e \"fs.copyFileSync('dist/esm/index.d.ts', 'dist/cjs/index.d.cts')\"",
+ "build": "rollup -c && npm run build:dedupe-types && tsc -p tsconfig.esm.json && npm run build:cts",
+ "test:jsr": "npx jsr@latest publish --dry-run",
+ "pretest": "npm run build",
+ "lint": "eslint",
+ "test": "mocha tests/**/*.js",
+ "test:coverage": "c8 npm test"
+ },
+ "keywords": [
+ "eslint",
+ "eslint-plugin",
+ "eslintplugin",
+ "json",
+ "linting"
+ ],
+ "license": "Apache-2.0",
+ "devDependencies": {
+ "@eslint/core": "^0.1.0",
+ "@types/eslint": "^8.56.10",
+ "c8": "^9.1.0",
+ "eslint": "^9.6.0",
+ "eslint-config-eslint": "^11.0.0",
+ "lint-staged": "^15.2.7",
+ "mocha": "^10.4.0",
+ "prettier": "^3.3.2",
+ "rollup": "^4.16.2",
+ "rollup-plugin-copy": "^3.5.0",
+ "typescript": "^5.4.5",
+ "yorkie": "^2.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "dependencies": {
+ "@humanwhocodes/momoa": "^3.0.6"
+ },
+ "peerDependencies": {
+ "eslint": "^9.6.0"
+ }
+}
diff --git a/prettier.config.js b/prettier.config.js
new file mode 100644
index 0000000..c334317
--- /dev/null
+++ b/prettier.config.js
@@ -0,0 +1,15 @@
+export default {
+ useTabs: true,
+ tabWidth: 4,
+ arrowParens: "avoid",
+
+ overrides: [
+ {
+ files: ["*.json"],
+ options: {
+ tabWidth: 2,
+ useTabs: false,
+ },
+ },
+ ],
+};
diff --git a/release-please-config.json b/release-please-config.json
new file mode 100644
index 0000000..a6f7b9f
--- /dev/null
+++ b/release-please-config.json
@@ -0,0 +1,16 @@
+{
+ "bootstrap-sha": "505795a1312e90f1ce3b59b530622929bc38b4fe",
+ "bump-minor-pre-major": true,
+ "packages": {
+ ".": {
+ "release-type": "node",
+ "extra-files": [
+ {
+ "type": "json",
+ "path": "jsr.json",
+ "jsonpath": "$.version"
+ }
+ ]
+ }
+ }
+}
diff --git a/rollup.config.js b/rollup.config.js
new file mode 100644
index 0000000..0ec27fb
--- /dev/null
+++ b/rollup.config.js
@@ -0,0 +1,14 @@
+export default {
+ input: "src/index.js",
+ output: [
+ {
+ file: "dist/cjs/index.cjs",
+ format: "cjs",
+ },
+ {
+ file: "dist/esm/index.js",
+ format: "esm",
+ banner: '// @ts-self-types="./index.d.ts"',
+ },
+ ],
+};
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..9618ba6
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,40 @@
+/**
+ * @fileoverview JSON plugin.
+ * @author Nicholas C. Zakas
+ */
+
+//-----------------------------------------------------------------------------
+// Imports
+//-----------------------------------------------------------------------------
+
+import { JSONLanguage } from "./languages/json-language.js";
+import noDuplicateKeys from "./rules/no-duplicate-keys.js";
+import noEmptyKeys from "./rules/no-empty-keys.js";
+
+//-----------------------------------------------------------------------------
+// Plugin
+//-----------------------------------------------------------------------------
+
+const plugin = {
+ languages: {
+ json: new JSONLanguage({ mode: "json" }),
+ jsonc: new JSONLanguage({ mode: "jsonc" }),
+ },
+ rules: {
+ "no-duplicate-keys": noDuplicateKeys,
+ "no-empty-keys": noEmptyKeys,
+ },
+ configs: {},
+};
+
+Object.assign(plugin.configs, {
+ recommended: {
+ plugins: { json: plugin },
+ rules: {
+ "json/no-duplicate-keys": "error",
+ "json/no-empty-keys": "error",
+ },
+ },
+});
+
+export default plugin;
diff --git a/src/languages/json-language.js b/src/languages/json-language.js
new file mode 100644
index 0000000..1b87fec
--- /dev/null
+++ b/src/languages/json-language.js
@@ -0,0 +1,141 @@
+/**
+ * @filedescription Functions to fix up rules to provide missing methods on the `context` object.
+ * @author Nicholas C. Zakas
+ */
+
+//------------------------------------------------------------------------------
+// Imports
+//------------------------------------------------------------------------------
+
+import { parse } from "@humanwhocodes/momoa";
+import { JSONSourceCode } from "./json-source-code.js";
+
+//-----------------------------------------------------------------------------
+// Types
+//-----------------------------------------------------------------------------
+
+/** @typedef {import("@humanwhocodes/momoa").DocumentNode} DocumentNode */
+/** @typedef {import("@eslint/core").Language} Language */
+/** @typedef {import("@eslint/core").File} File */
+/** @typedef {import("@eslint/core").ParseResult} ParseResult */
+/** @typedef {import("@eslint/core").SyntaxElement} SyntaxElement */
+
+//-----------------------------------------------------------------------------
+// Exports
+//-----------------------------------------------------------------------------
+
+/**
+ * JSON Language Object
+ * @implements {Language}
+ */
+export class JSONLanguage {
+ /**
+ * The type of file to read.
+ * @type {"text"}
+ */
+ fileType = "text";
+
+ /**
+ * The line number at which the parser starts counting.
+ * @type {0|1}
+ */
+ lineStart = 1;
+
+ /**
+ * The column number at which the parser starts counting.
+ * @type {0|1}
+ */
+ columnStart = 1;
+
+ /**
+ * The name of the key that holds the type of the node.
+ * @type {string}
+ */
+ nodeTypeKey = "type";
+
+ /**
+ * The parser mode.
+ * @type {"json"|"jsonc"}
+ */
+ #mode = "json";
+
+ /**
+ * Creates a new instance.
+ * @param {Object} options The options to use for this instance.
+ * @param {"json"|"jsonc"} options.mode The parser mode to use.
+ */
+ constructor({ mode }) {
+ this.#mode = mode;
+ }
+
+ /* eslint-disable class-methods-use-this, no-unused-vars -- Required to complete interface. */
+ /**
+ * Validates the language options.
+ * @param {Object} languageOptions The language options to validate.
+ * @returns {void}
+ * @throws {Error} When the language options are invalid.
+ */
+ validateLanguageOptions(languageOptions) {
+ // no-op
+ }
+ /* eslint-enable class-methods-use-this, no-unused-vars -- Required to complete interface. */
+
+ /**
+ * Parses the given file into an AST.
+ * @param {File} file The virtual file to parse.
+ * @returns {ParseResult} The result of parsing.
+ */
+ parse(file) {
+ // Note: BOM already removed
+ const text = /** @type {string} */ (file.body);
+
+ /*
+ * Check for parsing errors first. If there's a parsing error, nothing
+ * else can happen. However, a parsing error does not throw an error
+ * from this method - it's just considered a fatal error message, a
+ * problem that ESLint identified just like any other.
+ */
+ try {
+ const root = parse(text, {
+ mode: this.#mode,
+ ranges: true,
+ tokens: true,
+ });
+
+ return {
+ ok: true,
+ ast: /** @type {DocumentNode & SyntaxElement} */ (root),
+ };
+ } catch (ex) {
+ // error messages end with (line:column) so we strip that off for ESLint
+ const message = ex.message
+ .slice(0, ex.message.lastIndexOf("("))
+ .trim();
+
+ return {
+ ok: false,
+ errors: [
+ {
+ ...ex,
+ message,
+ },
+ ],
+ };
+ }
+ }
+
+ /* eslint-disable class-methods-use-this -- Required to complete interface. */
+ /**
+ * Creates a new `JSONSourceCode` object from the given information.
+ * @param {File} file The virtual file to create a `JSONSourceCode` object from.
+ * @param {ParseResult} parseResult The result returned from `parse()`.
+ * @returns {JSONSourceCode} The new `JSONSourceCode` object.
+ */
+ createSourceCode(file, parseResult) {
+ return new JSONSourceCode({
+ text: /** @type {string} */ (file.body),
+ ast: parseResult.ast,
+ });
+ }
+ /* eslint-enable class-methods-use-this -- Required to complete interface. */
+}
diff --git a/src/languages/json-source-code.js b/src/languages/json-source-code.js
new file mode 100644
index 0000000..d207815
--- /dev/null
+++ b/src/languages/json-source-code.js
@@ -0,0 +1,233 @@
+/**
+ * @fileoverview The JSONSourceCode class.
+ * @author Nicholas C. Zakas
+ */
+
+//-----------------------------------------------------------------------------
+// Imports
+//-----------------------------------------------------------------------------
+
+import { iterator } from "@humanwhocodes/momoa";
+
+//-----------------------------------------------------------------------------
+// Types
+//-----------------------------------------------------------------------------
+
+/** @typedef {import("@humanwhocodes/momoa").DocumentNode} DocumentNode */
+/** @typedef {import("@humanwhocodes/momoa").Node} JSONNode */
+/** @typedef {import("@humanwhocodes/momoa").Token} JSONToken */
+/** @typedef {import("@eslint/core").SyntaxElement} SyntaxElement */
+/** @typedef {import("@eslint/core").Language} Language */
+/** @typedef {import("@eslint/core").File} File */
+/** @typedef {import("@eslint/core").TraversalStep} TraversalStep */
+/** @typedef {import("@eslint/core").VisitTraversalStep} VisitTraversalStep */
+/** @typedef {import("@eslint/core").TextSourceCode} TextSourceCode */
+/** @typedef {import("@eslint/core").ParseResult} ParseResult */
+
+//-----------------------------------------------------------------------------
+// Helpers
+//-----------------------------------------------------------------------------
+
+/**
+ * A class to represent a step in the traversal process.
+ * @implements {VisitTraversalStep}
+ */
+class JSONTraversalStep {
+ /**
+ * The type of the step.
+ * @type {"visit"}
+ * @readonly
+ */
+ type = "visit";
+
+ /**
+ * The kind of the step. Represents the same data as the `type` property
+ * but it's a number for performance.
+ * @type {1}
+ * @readonly
+ */
+ kind = 1;
+
+ /**
+ * The target of the step.
+ * @type {JSONNode & SyntaxElement}
+ */
+ target;
+
+ /**
+ * The phase of the step.
+ * @type {1|2}
+ */
+ phase;
+
+ /**
+ * The arguments of the step.
+ * @type {Array}
+ */
+ args;
+
+ /**
+ * Creates a new instance.
+ * @param {Object} options The options for the step.
+ * @param {JSONNode & SyntaxElement} options.target The target of the step.
+ * @param {1|2} options.phase The phase of the step.
+ * @param {Array} options.args The arguments of the step.
+ */
+ constructor({ target, phase, args }) {
+ this.target = target;
+ this.phase = phase;
+ this.args = args;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Exports
+//-----------------------------------------------------------------------------
+
+/**
+ * JSON Source Code Object
+ * @implements {TextSourceCode}
+ */
+export class JSONSourceCode {
+ /**
+ * Cached traversal steps.
+ * @type {Array|undefined}
+ */
+ #steps;
+
+ /**
+ * Cache of parent nodes.
+ * @type {WeakMap}
+ */
+ #parents = new WeakMap();
+
+ /**
+ * The lines of text in the source code.
+ * @type {Array}
+ */
+ #lines;
+
+ /**
+ * The AST of the source code.
+ * @type {DocumentNode & SyntaxElement}
+ */
+ ast;
+
+ /**
+ * The text of the source code.
+ * @type {string}
+ */
+ text;
+
+ /**
+ * The comment node in the source code.
+ * @type {Array|undefined}
+ */
+ comments;
+
+ /**
+ * Creates a new instance.
+ * @param {Object} options The options for the instance.
+ * @param {string} options.text The source code text.
+ * @param {DocumentNode & SyntaxElement} options.ast The root AST node.
+ */
+ constructor({ text, ast }) {
+ this.ast = ast;
+ this.text = text;
+ this.comments = ast.tokens.filter(token =>
+ token.type.endsWith("Comment"),
+ );
+ }
+
+ /**
+ * Returns the parent of the given node.
+ * @param {JSONNode} node The node to get the parent of.
+ * @returns {JSONNode|undefined} The parent of the node.
+ */
+ getParent(node) {
+ return this.#parents.get(node);
+ }
+
+ /**
+ * Gets all the ancestors of a given node
+ * @param {JSONNode} node The node
+ * @returns {Array} All the ancestor nodes in the AST, not including the provided node, starting
+ * from the root node at index 0 and going inwards to the parent node.
+ * @throws {TypeError} When `node` is missing.
+ */
+ getAncestors(node) {
+ if (!node) {
+ throw new TypeError("Missing required argument: node.");
+ }
+
+ const ancestorsStartingAtParent = [];
+
+ for (
+ let ancestor = this.#parents.get(node);
+ ancestor;
+ ancestor = this.#parents.get(ancestor)
+ ) {
+ ancestorsStartingAtParent.push(ancestor);
+ }
+
+ return ancestorsStartingAtParent.reverse();
+ }
+
+ /**
+ * Gets the source code for the given node.
+ * @param {JSONNode} [node] The AST node to get the text for.
+ * @param {number} [beforeCount] The number of characters before the node to retrieve.
+ * @param {number} [afterCount] The number of characters after the node to retrieve.
+ * @returns {string} The text representing the AST node.
+ * @public
+ */
+ getText(node, beforeCount, afterCount) {
+ if (node) {
+ return this.text.slice(
+ Math.max(node.range[0] - (beforeCount || 0), 0),
+ node.range[1] + (afterCount || 0),
+ );
+ }
+ return this.text;
+ }
+
+ /**
+ * Gets the entire source text split into an array of lines.
+ * @returns {Array} The source text as an array of lines.
+ * @public
+ */
+ get lines() {
+ if (!this.#lines) {
+ this.#lines = this.text.split(/\r?\n/gu);
+ }
+ return this.#lines;
+ }
+
+ /**
+ * Traverse the source code and return the steps that were taken.
+ * @returns {Iterable} The steps that were taken while traversing the source code.
+ */
+ traverse() {
+ // Because the AST doesn't mutate, we can cache the steps
+ if (this.#steps) {
+ return this.#steps.values();
+ }
+
+ const steps = (this.#steps = []);
+
+ for (const { node, parent, phase } of iterator(
+ /** @type {DocumentNode} */ (this.ast),
+ )) {
+ this.#parents.set(node, parent);
+ steps.push(
+ new JSONTraversalStep({
+ target: /** @type {JSONNode & SyntaxElement} */ (node),
+ phase: phase === "enter" ? 1 : 2,
+ args: [node, parent],
+ }),
+ );
+ }
+
+ return steps;
+ }
+}
diff --git a/src/rules/no-duplicate-keys.js b/src/rules/no-duplicate-keys.js
new file mode 100644
index 0000000..336fb41
--- /dev/null
+++ b/src/rules/no-duplicate-keys.js
@@ -0,0 +1,53 @@
+/**
+ * @fileoverview Rule to prevent duplicate keys in JSON.
+ * @author Nicholas C. Zakas
+ */
+
+//-----------------------------------------------------------------------------
+// Type Definitions
+//-----------------------------------------------------------------------------
+
+export default {
+ meta: {
+ type: "problem",
+
+ docs: {
+ description: "Disallow duplicate keys in JSON objects",
+ },
+
+ messages: {
+ duplicateKey: 'Duplicate key "{{key}}" found.',
+ },
+ },
+
+ create(context) {
+ const objectKeys = [];
+ let keys;
+
+ return {
+ Object() {
+ objectKeys.push(keys);
+ keys = new Map();
+ },
+
+ Member(node) {
+ const key = node.name.value;
+
+ if (keys.has(key)) {
+ context.report({
+ loc: node.name.loc,
+ messageId: "duplicateKey",
+ data: {
+ key,
+ },
+ });
+ } else {
+ keys.set(key, node);
+ }
+ },
+ "Object:exit"() {
+ keys = objectKeys.pop();
+ },
+ };
+ },
+};
diff --git a/src/rules/no-empty-keys.js b/src/rules/no-empty-keys.js
new file mode 100644
index 0000000..85e103b
--- /dev/null
+++ b/src/rules/no-empty-keys.js
@@ -0,0 +1,33 @@
+/**
+ * @fileoverview Rule to prevent empty keys in JSON.
+ * @author Nicholas C. Zakas
+ */
+
+export default {
+ meta: {
+ type: "problem",
+
+ docs: {
+ description: "Disallow empty keys in JSON objects",
+ },
+
+ messages: {
+ emptyKey: "Empty key found.",
+ },
+ },
+
+ create(context) {
+ return {
+ Member(node) {
+ const key = node.name.value;
+
+ if (key.trim() === "") {
+ context.report({
+ loc: node.name.loc,
+ messageId: "emptyKey",
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tests/languages/json-language.test.js b/tests/languages/json-language.test.js
new file mode 100644
index 0000000..ca02533
--- /dev/null
+++ b/tests/languages/json-language.test.js
@@ -0,0 +1,90 @@
+/**
+ * @fileoverview Tests for JSONLanguage
+ * @author Nicholas C. Zakas
+ */
+
+//-----------------------------------------------------------------------------
+// Imports
+//-----------------------------------------------------------------------------
+
+import { JSONLanguage } from "../../src/languages/json-language.js";
+import assert from "node:assert";
+
+//-----------------------------------------------------------------------------
+// Tests
+//-----------------------------------------------------------------------------
+
+describe("JSONLanguage", () => {
+ describe("parse()", () => {
+ it("should not parse jsonc by default", () => {
+ const language = new JSONLanguage({ mode: "json" });
+ const result = language.parse({
+ body: "{\n//test\n}",
+ path: "test.json",
+ });
+
+ assert.strictEqual(result.ok, false);
+ assert.strictEqual(
+ result.errors[0].message,
+ "Unexpected character '/' found.",
+ );
+ });
+
+ it("should parse json by default", () => {
+ const language = new JSONLanguage({ mode: "json" });
+ const result = language.parse({
+ body: "{\n\n}",
+ path: "test.json",
+ });
+
+ assert.strictEqual(result.ok, true);
+ assert.strictEqual(result.ast.type, "Document");
+ assert.strictEqual(result.ast.body.type, "Object");
+ });
+
+ it("should set the mode to jsonc", () => {
+ const language = new JSONLanguage({ mode: "jsonc" });
+ const result = language.parse({
+ body: "{\n//test\n}",
+ path: "test.jsonc",
+ });
+
+ assert.strictEqual(result.ok, true);
+ assert.strictEqual(result.ast.type, "Document");
+ assert.strictEqual(result.ast.body.type, "Object");
+ });
+ });
+
+ describe("createSourceCode()", () => {
+ it("should create a JSONSourceCode instance for JSON", () => {
+ const language = new JSONLanguage({ mode: "json" });
+ const file = { body: "{\n\n}", path: "test.json" };
+ const parseResult = language.parse(file);
+ const sourceCode = language.createSourceCode(file, parseResult);
+ assert.strictEqual(sourceCode.constructor.name, "JSONSourceCode");
+
+ assert.strictEqual(sourceCode.ast.type, "Document");
+ assert.strictEqual(sourceCode.ast.body.type, "Object");
+ assert.strictEqual(sourceCode.text, "{\n\n}");
+ assert.strictEqual(sourceCode.comments.length, 0);
+ });
+
+ it("should create a JSONSourceCode instance for JSONC", () => {
+ const language = new JSONLanguage({ mode: "jsonc" });
+ const file = { body: "{\n//test\n}", path: "test.jsonc" };
+ const parseResult = language.parse(file);
+ const sourceCode = language.createSourceCode(
+ file,
+ parseResult,
+ "test.jsonc",
+ );
+
+ assert.strictEqual(sourceCode.constructor.name, "JSONSourceCode");
+
+ assert.strictEqual(sourceCode.ast.type, "Document");
+ assert.strictEqual(sourceCode.ast.body.type, "Object");
+ assert.strictEqual(sourceCode.text, "{\n//test\n}");
+ assert.strictEqual(sourceCode.comments.length, 1);
+ });
+ });
+});
diff --git a/tests/languages/json-source-code.test.js b/tests/languages/json-source-code.test.js
new file mode 100644
index 0000000..261bb08
--- /dev/null
+++ b/tests/languages/json-source-code.test.js
@@ -0,0 +1,103 @@
+/**
+ * @fileoverview Tests for JSONSourceCode
+ * @author Nicholas C. Zakas
+ */
+
+//-----------------------------------------------------------------------------
+// Imports
+//-----------------------------------------------------------------------------
+
+import { JSONSourceCode } from "../../src/languages/json-source-code.js";
+import { JSONLanguage } from "../../src/languages/json-language.js";
+import assert from "node:assert";
+
+//-----------------------------------------------------------------------------
+// Tests
+//-----------------------------------------------------------------------------
+
+describe("JSONSourceCode", () => {
+ describe("constructor", () => {
+ it("should create a JSONSourceCode instance", () => {
+ const ast = {
+ type: "Document",
+ body: {
+ type: "Object",
+ properties: [],
+ },
+ tokens: [],
+ };
+ const text = "{}";
+ const sourceCode = new JSONSourceCode({
+ text,
+ ast,
+ });
+
+ assert.strictEqual(sourceCode.constructor.name, "JSONSourceCode");
+ assert.strictEqual(sourceCode.ast, ast);
+ assert.strictEqual(sourceCode.text, text);
+ });
+ });
+
+ describe("getText()", () => {
+ it("should return the text of the source code", () => {
+ const file = { body: "{}", path: "test.json" };
+ const language = new JSONLanguage({ mode: "json" });
+ const parseResult = language.parse(file);
+ const sourceCode = new JSONSourceCode({
+ text: file.body,
+ ast: parseResult.ast,
+ });
+
+ assert.strictEqual(sourceCode.getText(), file.body);
+ });
+ });
+
+ describe("comments", () => {
+ it("should contain an empty array when parsing JSON", () => {
+ const file = { body: "{}", path: "test.json" };
+ const language = new JSONLanguage({ mode: "json" });
+ const parseResult = language.parse(file);
+ const sourceCode = new JSONSourceCode({
+ text: file.body,
+ ast: parseResult.ast,
+ });
+
+ assert.deepStrictEqual(sourceCode.comments, []);
+ });
+
+ it("should contain an array of comments when parsing JSONC", () => {
+ const file = { body: "{\n//test\n}", path: "test.jsonc" };
+ const language = new JSONLanguage({ mode: "jsonc" });
+ const parseResult = language.parse(file);
+ const sourceCode = new JSONSourceCode({
+ text: file.body,
+ ast: parseResult.ast,
+ });
+
+ // should contain one comment
+ assert.strictEqual(sourceCode.comments.length, 1);
+
+ const comment = sourceCode.comments[0];
+ assert.strictEqual(comment.type, "LineComment");
+ assert.deepStrictEqual(comment.range, [2, 8]);
+ assert.deepStrictEqual(comment.loc, {
+ start: { line: 2, column: 1, offset: 2 },
+ end: { line: 2, column: 7, offset: 8 },
+ });
+ });
+ });
+
+ describe("get lines", () => {
+ it("should return an array of lines", () => {
+ const file = { body: "{\n//test\n}", path: "test.jsonc" };
+ const language = new JSONLanguage({ mode: "jsonc" });
+ const parseResult = language.parse(file);
+ const sourceCode = new JSONSourceCode({
+ text: file.body,
+ ast: parseResult.ast,
+ });
+
+ assert.deepStrictEqual(sourceCode.lines, ["{", "//test", "}"]);
+ });
+ });
+});
diff --git a/tests/rules/no-duplicate-keys.test.js b/tests/rules/no-duplicate-keys.test.js
new file mode 100644
index 0000000..34a5334
--- /dev/null
+++ b/tests/rules/no-duplicate-keys.test.js
@@ -0,0 +1,65 @@
+/**
+ * @fileoverview Tests for no-duplicate-keys rule.
+ * @author Nicholas C. Zakas
+ */
+
+//------------------------------------------------------------------------------
+// Imports
+//------------------------------------------------------------------------------
+
+import rule from "../../src/rules/no-duplicate-keys.js";
+import json from "../../src/index.js";
+import { RuleTester } from "eslint";
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({
+ plugins: {
+ json,
+ },
+ language: "json/json",
+});
+
+ruleTester.run("no-duplicate-keys", rule, {
+ valid: [
+ '{"foo": 1, "bar": 2}',
+ '{"foo": 1, "bar": 2, "baz": 3}',
+ "[]",
+ "{}",
+ '{"foo": 1, "bar": {"bar": 2}}',
+ '{"foo": { "bar": 5 }, "bar": 6 }',
+ ],
+ invalid: [
+ {
+ code: '{"foo": 1, "foo": 2}',
+ errors: [
+ {
+ messageId: "duplicateKey",
+ line: 1,
+ column: 12,
+ endLine: 1,
+ endColumn: 17,
+ },
+ ],
+ },
+ {
+ code: `{
+ "foo": {
+ "bar": 5
+ },
+ "foo": 6
+}`,
+ errors: [
+ {
+ messageId: "duplicateKey",
+ line: 5,
+ column: 5,
+ endLine: 5,
+ endColumn: 10,
+ },
+ ],
+ },
+ ],
+});
diff --git a/tests/rules/no-empty-keys.test.js b/tests/rules/no-empty-keys.test.js
new file mode 100644
index 0000000..c7a6065
--- /dev/null
+++ b/tests/rules/no-empty-keys.test.js
@@ -0,0 +1,53 @@
+/**
+ * @fileoverview Tests for no-empty-keys rule.
+ * @author Nicholas C. Zakas
+ */
+
+//------------------------------------------------------------------------------
+// Imports
+//------------------------------------------------------------------------------
+
+import rule from "../../src/rules/no-empty-keys.js";
+import json from "../../src/index.js";
+import { RuleTester } from "eslint";
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({
+ plugins: {
+ json,
+ },
+ language: "json/json",
+});
+
+ruleTester.run("no-empty-keys", rule, {
+ valid: ['{"foo": 1, "bar": 2}'],
+ invalid: [
+ {
+ code: '{"": 1}',
+ errors: [
+ {
+ messageId: "emptyKey",
+ line: 1,
+ column: 2,
+ endLine: 1,
+ endColumn: 4,
+ },
+ ],
+ },
+ {
+ code: '{" ": 1}',
+ errors: [
+ {
+ messageId: "emptyKey",
+ line: 1,
+ column: 2,
+ endLine: 1,
+ endColumn: 6,
+ },
+ ],
+ },
+ ],
+});
diff --git a/tools/dedupe-types.js b/tools/dedupe-types.js
new file mode 100644
index 0000000..ce8b16d
--- /dev/null
+++ b/tools/dedupe-types.js
@@ -0,0 +1,43 @@
+/**
+ * @fileoverview Strips typedef aliases from the rolled-up file. This
+ * is necessary because the TypeScript compiler throws an error when
+ * it encounters a duplicate typedef.
+ *
+ * Usage:
+ * node scripts/strip-typedefs.js filename1.js filename2.js ...
+ *
+ * @author Nicholas C. Zakas
+ */
+
+//-----------------------------------------------------------------------------
+// Imports
+//-----------------------------------------------------------------------------
+
+import fs from "node:fs";
+
+//-----------------------------------------------------------------------------
+// Main
+//-----------------------------------------------------------------------------
+
+// read files from the command line
+const files = process.argv.slice(2);
+
+files.forEach(filePath => {
+ const lines = fs.readFileSync(filePath, "utf8").split(/\r?\n/gu);
+ const typedefs = new Set();
+
+ const remainingLines = lines.filter(line => {
+ if (!line.startsWith("/** @typedef {import")) {
+ return true;
+ }
+
+ if (typedefs.has(line)) {
+ return false;
+ }
+
+ typedefs.add(line);
+ return true;
+ });
+
+ fs.writeFileSync(filePath, remainingLines.join("\n"), "utf8");
+});
diff --git a/tsconfig.esm.json b/tsconfig.esm.json
new file mode 100644
index 0000000..40ece13
--- /dev/null
+++ b/tsconfig.esm.json
@@ -0,0 +1,4 @@
+{
+ "extends": "./tsconfig.json",
+ "files": ["dist/esm/index.js"]
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..3fa504c
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "files": ["src/index.js"],
+ "compilerOptions": {
+ "declaration": true,
+ "emitDeclarationOnly": true,
+ "allowJs": true,
+ "checkJs": true,
+ "outDir": "dist/esm",
+ "target": "ES2022",
+ "moduleResolution": "NodeNext",
+ "module": "NodeNext"
+ }
+}