From 19a2d551f50a64c7f5206061b39c1c8bff25a1e8 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 23 Jul 2024 16:06:35 -0400 Subject: [PATCH 1/3] chore: Add type checking --- package.json | 15 +++++++++++---- rollup.config.js | 10 ++++++++++ tools/dedupe-types.js | 43 +++++++++++++++++++++++++++++++++++++++++++ tsconfig.esm.json | 4 ++++ tsconfig.json | 13 +++++++++++++ 5 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 rollup.config.js create mode 100644 tools/dedupe-types.js create mode 100644 tsconfig.esm.json create mode 100644 tsconfig.json diff --git a/package.json b/package.json index e2479d74..14ccfebd 100644 --- a/package.json +++ b/package.json @@ -8,14 +8,15 @@ "url": "https://github.com/btmills" }, "type": "module", - "main": "src/index.js", + "main": "dist/esm/index.js", + "types": "dist/esm/index.d.ts", "exports": { "import": { - "default": "./src/index.js" + "default": "./dist/esm/index.js" } }, "files": [ - "src" + "dist" ], "publishConfig": { "access": "public" @@ -34,18 +35,24 @@ ], "scripts": { "lint": "eslint .", + "build:dedupe-types": "node tools/dedupe-types.js dist/esm/index.js", + "build": "rollup -c && npm run build:dedupe-types && tsc -p tsconfig.esm.json", "prepare": "node ./npm-prepare.cjs", "test": "c8 mocha \"tests/**/*.test.js\" --timeout 30000" }, "devDependencies": { "@eslint/core": "^0.2.0", "@eslint/js": "^9.4.0", + "@types/eslint": "^9.6.0", "c8": "^10.1.2", "chai": "^5.1.1", "eslint": "^9.4.0", "eslint-config-eslint": "^11.0.0", "globals": "^15.1.0", - "mocha": "^10.6.0" + "mocha": "^10.6.0", + "rollup": "^4.19.0", + "rollup-plugin-copy": "^3.5.0", + "typescript": "^5.5.4" }, "dependencies": { "mdast-util-from-markdown": "^2.0.1" diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 00000000..18e23f22 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,10 @@ +export default { + input: "src/index.js", + output: [ + { + file: "dist/esm/index.js", + format: "esm", + banner: '// @ts-self-types="./index.d.ts"' + } + ] +}; diff --git a/tools/dedupe-types.js b/tools/dedupe-types.js new file mode 100644 index 00000000..ce8b16d1 --- /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 00000000..40ece132 --- /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 00000000..3fa504c2 --- /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" + } +} From 5f48e1995c55e7f54ee0463203b917126aeee360 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 23 Jul 2024 16:56:16 -0400 Subject: [PATCH 2/3] feat: Add type checking --- .gitignore | 1 + rollup.config.js | 9 ++++++ src/index.js | 9 ++++++ src/processor.js | 76 ++++++++++++++++++++++++++++-------------------- src/types.ts | 19 ++++++++++++ tsconfig.json | 1 + 6 files changed, 84 insertions(+), 31 deletions(-) create mode 100644 src/types.ts diff --git a/.gitignore b/.gitignore index 05c7c9ea..9c5c5a93 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ npm-debug.log yarn.lock package-lock.json pnpm-lock.yaml +dist diff --git a/rollup.config.js b/rollup.config.js index 18e23f22..5dfdd45c 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,3 +1,5 @@ +import copy from "rollup-plugin-copy"; + export default { input: "src/index.js", output: [ @@ -6,5 +8,12 @@ export default { format: "esm", banner: '// @ts-self-types="./index.d.ts"' } + ], + plugins: [ + copy({ + targets: [ + { src: "src/types.ts", dest: "dist/esm" } + ] + }) ] }; diff --git a/src/index.js b/src/index.js index 7de920c2..9f334ab2 100644 --- a/src/index.js +++ b/src/index.js @@ -9,10 +9,18 @@ import { processor } from "./processor.js"; +//----------------------------------------------------------------------------- +// Type Definitions +//----------------------------------------------------------------------------- + +/** @typedef {import("eslint").Linter.RulesRecord} RulesRecord*/ +/** @typedef {import("eslint").ESLint.Plugin} Plugin */ + //----------------------------------------------------------------------------- // Exports //----------------------------------------------------------------------------- +/** @type {RulesRecord} */ const rulesConfig = { // The Markdown parser automatically trims trailing @@ -36,6 +44,7 @@ const rulesConfig = { "unicode-bom": "off" }; +/** @type {Plugin} */ const plugin = { meta: { name: "@eslint/markdown", diff --git a/src/processor.js b/src/processor.js index 7712d373..a90c3007 100644 --- a/src/processor.js +++ b/src/processor.js @@ -3,30 +3,30 @@ * @author Brandon Mills */ -/** - * @typedef {import('eslint/lib/shared/types').LintMessage} Message - * @typedef {Object} ASTNode - * @property {string} type The type of node. - * @property {string} [lang] The language that the node is in - * @typedef {Object} RangeMap - * @property {number} indent Number of code block indent characters trimmed from - * the beginning of the line during extraction. - * @property {number} js Offset from the start of the code block's range in the - * extracted JS. - * @property {number} md Offset from the start of the code block's range in the - * original Markdown. - * @typedef {Object} BlockBase - * @property {string} baseIndentText Leading whitespace text for the block. - * @property {string[]} comments Comments inside of the JavaScript code. - * @property {RangeMap[]} rangeMap A list of offset-based adjustments, where - * lookups are done based on the `js` key, which represents the range in the - * linted JS, and the `md` key is the offset delta that, when added to the JS - * range, returns the corresponding location in the original Markdown source. - * @typedef {ASTNode & BlockBase} Block - */ +//----------------------------------------------------------------------------- +// Imports +//----------------------------------------------------------------------------- import { fromMarkdown } from "mdast-util-from-markdown"; +//----------------------------------------------------------------------------- +// Type Definitions +//----------------------------------------------------------------------------- + +/** @typedef {import("./types.ts").Block} Block */ +/** @typedef {import("./types.ts").RangeMap} RangeMap */ +/** @typedef {import("mdast").Node} Node */ +/** @typedef {import("mdast").Parent} ParentNode */ +/** @typedef {import("mdast").Code} CodeNode */ +/** @typedef {import("mdast").Html} HtmlNode */ +/** @typedef {import("eslint").Linter.LintMessage} Message */ +/** @typedef {import("eslint").Rule.Fix} Fix */ +/** @typedef {import("eslint").AST.Range} Range */ + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + const UNSATISFIABLE_RULES = new Set([ "eol-last", // The Markdown parser strips trailing newlines in code fences "unicode-bom" // Code blocks will begin in the middle of Markdown files @@ -40,8 +40,8 @@ const blocksCache = new Map(); /** * Performs a depth-first traversal of the Markdown AST. - * @param {ASTNode} node A Markdown AST node. - * @param {{[key: string]: (node: ASTNode) => void}} callbacks A map of node types to callbacks. + * @param {Node} node A Markdown AST node. + * @param {{[key: string]: (node?: Node) => void}} callbacks A map of node types to callbacks. * @returns {void} */ function traverse(node, callbacks) { @@ -51,9 +51,11 @@ function traverse(node, callbacks) { callbacks["*"](); } - if (typeof node.children !== "undefined") { - for (let i = 0; i < node.children.length; i++) { - traverse(node.children[i], callbacks); + const parent = /** @type {ParentNode} */ (node); + + if (typeof parent.children !== "undefined") { + for (let i = 0; i < parent.children.length; i++) { + traverse(parent.children[i], callbacks); } } } @@ -92,7 +94,7 @@ const leadingWhitespaceRegex = /^[>\s]*/u; /** * Gets the offset for the first column of the node's first line in the * original source text. - * @param {ASTNode} node A Markdown code block AST node. + * @param {Node} node A Markdown code block AST node. * @returns {number} The offset for the first column of the node's first line. */ function getBeginningOfLineOffset(node) { @@ -103,7 +105,7 @@ function getBeginningOfLineOffset(node) { * Gets the leading text, typically whitespace with possible blockquote chars, * used to indent a code block. * @param {string} text The text of the file. - * @param {ASTNode} node A Markdown code block AST node. + * @param {Node} node A Markdown code block AST node. * @returns {string} The text from the start of the first line to the opening * fence of the code block. */ @@ -137,7 +139,7 @@ function getIndentText(text, node) { * differences within the line, so the mapping need only provide the offset * delta at the beginning of each line. * @param {string} text The text of the file. - * @param {ASTNode} node A Markdown code block AST node. + * @param {Node} node A Markdown code block AST node. * @param {string[]} comments List of configuration comment strings that will be * inserted at the beginning of the code block. * @returns {RangeMap[]} A list of offset-based adjustments, where lookups are @@ -265,6 +267,12 @@ function preprocess(text, filename) { "*"() { htmlComments = []; }, + + /** + * Visit a code node. + * @param {CodeNode} node The visited node. + * @returns {void} + */ code(node) { if (node.lang) { const comments = []; @@ -288,6 +296,12 @@ function preprocess(text, filename) { }); } }, + + /** + * Visit an HTML node. + * @param {HtmlNode} node The visited node. + * @returns {void} + */ html(node) { const comment = getComment(node.value); @@ -357,7 +371,7 @@ function adjustBlock(block) { if (message.fix) { adjustedFix.fix = { - range: message.fix.range.map(range => { + range: /** @type {Range} */ (message.fix.range.map(range => { // Advance through the block's range map to find the last // matching range by finding the first range too far and @@ -370,7 +384,7 @@ function adjustBlock(block) { // Apply the mapping delta for this range. return range + block.rangeMap[i - 1].md; - }), + })), text: message.fix.text.replace(/\n/gu, `\n${block.baseIndentText}`) }; } diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..02638045 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,19 @@ +import type { Node } from "mdast"; +import type { Linter } from "eslint"; + + +export interface RangeMap { + indent: number; + js: number; + md: number; +} + +export interface BlockBase { + baseIndentText: string; + comments: string[]; + rangeMap: RangeMap[]; +} + +export interface Block extends Node, BlockBase {} + +export type Message = Linter.LintMessage; diff --git a/tsconfig.json b/tsconfig.json index 3fa504c2..04a726cc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "declaration": true, "emitDeclarationOnly": true, + "allowImportingTsExtensions": true, "allowJs": true, "checkJs": true, "outDir": "dist/esm", From 08ee506fdb7775e9ba17c8427bb142d7a77573cc Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 25 Jul 2024 10:56:42 -0400 Subject: [PATCH 3/3] Fix lint errors --- package.json | 1 + tools/dedupe-types.js | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 14ccfebd..555415e6 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ ], "scripts": { "lint": "eslint .", + "lint:fix": "eslint --fix .", "build:dedupe-types": "node tools/dedupe-types.js dist/esm/index.js", "build": "rollup -c && npm run build:dedupe-types && tsc -p tsconfig.esm.json", "prepare": "node ./npm-prepare.cjs", diff --git a/tools/dedupe-types.js b/tools/dedupe-types.js index ce8b16d1..e6bba030 100644 --- a/tools/dedupe-types.js +++ b/tools/dedupe-types.js @@ -23,21 +23,21 @@ import fs from "node:fs"; const files = process.argv.slice(2); files.forEach(filePath => { - const lines = fs.readFileSync(filePath, "utf8").split(/\r?\n/gu); - const typedefs = new Set(); + 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; - } + const remainingLines = lines.filter(line => { + if (!line.startsWith("/** @typedef {import")) { + return true; + } - if (typedefs.has(line)) { - return false; - } + if (typedefs.has(line)) { + return false; + } - typedefs.add(line); - return true; - }); + typedefs.add(line); + return true; + }); - fs.writeFileSync(filePath, remainingLines.join("\n"), "utf8"); + fs.writeFileSync(filePath, remainingLines.join("\n"), "utf8"); });