From d2ddc69517968c99be936dc4ae5a8eb482c2f9b0 Mon Sep 17 00:00:00 2001 From: John Hooks Date: Sun, 19 Mar 2023 19:51:23 -0700 Subject: [PATCH] WIP --- .babelrc | 13 ++-- .eslintrc.json | 14 +--- .gitignore | 1 - mocha-setup.js | 3 + package-lock.json | 46 +++++++++++ package.json | 21 ++++-- rollup.config.mjs | 8 +- src/index.ts | 176 ++++++++++++++++++++++++++++++++----------- tsconfig.eslint.json | 7 -- 9 files changed, 209 insertions(+), 80 deletions(-) create mode 100644 mocha-setup.js delete mode 100644 tsconfig.eslint.json diff --git a/.babelrc b/.babelrc index 29a1161..fa3808b 100644 --- a/.babelrc +++ b/.babelrc @@ -1,13 +1,16 @@ { "presets": [ - [ "@babel/env", { - "modules": false - } ], - "@babel/preset-typescript" + "@babel/preset-typescript", + [ + "@babel/env", + { + "modules": false + } + ] ], "env": { "test": { - "presets": [ "@babel/env" ] + "presets": ["@babel/preset-typescript", "@babel/env"] } } } diff --git a/.eslintrc.json b/.eslintrc.json index f9da10c..ebd81cd 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,20 +1,12 @@ { - "root": true, - "extends": ["eslint:recommended", "prettier", "plugin:@typescript-eslint/recommended"], - "plugins": ["prettier"], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 2020, - "sourceType": "module", - "project": ["./tsconfig.json", "./tsconfig.eslint.json"] - }, + "extends": "@aduth/eslint-config", "env": { - "es6": true, "browser": true, "mocha": true, "node": true }, "rules": { - "prettier/prettier": "error" + "no-redeclare": "off", + "@typescript-eslint/no-redeclare": "error" } } diff --git a/.gitignore b/.gitignore index 9d27333..3d5ae6a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ /es/* -!/es/index.d.ts node_modules/ *.log dist/ diff --git a/mocha-setup.js b/mocha-setup.js new file mode 100644 index 0000000..f437bb5 --- /dev/null +++ b/mocha-setup.js @@ -0,0 +1,3 @@ +import register from '@babel/register'; + +register({ extensions: ['.ts'] }); diff --git a/package-lock.json b/package-lock.json index a246c19..00764a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.3.0", "license": "MIT", "devDependencies": { + "@aduth/eslint-config": "^4.4.1", "@babel/cli": "^7.20.7", "@babel/core": "^7.20.12", "@babel/preset-env": "^7.20.2", @@ -30,6 +31,51 @@ "typescript": "^5.0.2" } }, + "node_modules/@aduth/eslint-config": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@aduth/eslint-config/-/eslint-config-4.4.1.tgz", + "integrity": "sha512-0DzJLhVxFtdBv8ONS1jgz/gbKDHBzv+piev07pSi5sgnMepxBGbFgRZeASVGyWo9njIQE6ExnCIvemittg44Lw==", + "dev": true, + "dependencies": { + "@aduth/is-dependency": "^1.0.0", + "deepmerge": "^4.2.2" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": ">=4.27.0", + "@typescript-eslint/parser": ">=4.27.0", + "eslint": ">=7.28.0", + "eslint-config-prettier": ">=8.3.0", + "eslint-plugin-jsdoc": ">=35.3.0", + "eslint-plugin-prettier": ">=3.4.0", + "typescript": ">=4.3.4" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "@typescript-eslint/parser": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + }, + "eslint-plugin-jsdoc": { + "optional": true + }, + "eslint-plugin-prettier": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@aduth/is-dependency": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@aduth/is-dependency/-/is-dependency-1.0.0.tgz", + "integrity": "sha512-1CJwTd6B6XDb6HETcHMwjRmQLQi2FgjM1rYUO6wf1yizKVPC9V66vmvVCtgePMzWmhsLqdhOPLWt+fNvG46FoA==", + "dev": true + }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", diff --git a/package.json b/package.json index b04b804..6d64aeb 100644 --- a/package.json +++ b/package.json @@ -19,14 +19,15 @@ "src" ], "scripts": { - "build:es": "babel src/ --extensions \".js,.ts,\" --out-dir es", + "build:es": "babel src/ --extensions '.ts' --out-dir es", "build:umd": "rollup -c", - "build:types": "tsc -b", - "build": "npm run build:es && npm run build:umd", + "build:types": "tsc -b tsconfig.decl.json", + "build": "npm run build:es && npm run build:umd && npm run build:types", "dev": "rollup -c -w", "lint": "eslint . --ignore-pattern dist --ignore-pattern es", - "unit-test": "NODE_ENV=test mocha --require @babel/register", - "test": "npm run unit-test && npm run lint", + "unit-test": "NODE_ENV=test mocha -r jsdom-global/register -r @babel/register -r ./mocha-setup.js --extension ts", + "typecheck": "tsc", + "test": "npm run unit-test && npm run lint && npm run typecheck", "prepublishOnly": "npm run build" }, "author": { @@ -36,6 +37,7 @@ }, "license": "MIT", "devDependencies": { + "@aduth/eslint-config": "^4.4.1", "@babel/cli": "^7.20.7", "@babel/core": "^7.20.12", "@babel/preset-env": "^7.20.2", @@ -44,13 +46,16 @@ "@rollup/plugin-babel": "^6.0.3", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-terser": "^0.4.0", - "@typescript-eslint/eslint-plugin": "^5.53.0", - "@typescript-eslint/parser": "^5.53.0", + "@types/chai": "^4.3.4", + "@types/mocha": "^10.0.1", + "@typescript-eslint/eslint-plugin": "^5.55.0", + "@typescript-eslint/parser": "^5.55.0", "chai": "^4.3.7", "eslint": "^8.34.0", - "eslint-config-prettier": "^8.6.0", + "eslint-config-prettier": "^8.7.0", "eslint-plugin-prettier": "^4.2.1", "jsdom": "^21.1.0", + "jsdom-global": "^3.0.2", "mocha": "^10.2.0", "prettier": "^2.8.4", "rollup": "^3.15.0", diff --git a/rollup.config.mjs b/rollup.config.mjs index 6efbef9..27f14fa 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -12,10 +12,10 @@ export default [ }, plugins: [ nodeResolve({ - extensions: ['.js', '.ts'], + extensions: ['.ts'], }), babel({ - extensions: ['.js', '.ts'], + extensions: ['.ts'], babelHelpers: 'bundled', exclude: 'node_modules/**', presets: ['@babel/preset-typescript'], @@ -31,10 +31,10 @@ export default [ }, plugins: [ nodeResolve({ - extensions: ['.js', '.ts'], + extensions: ['.ts'], }), babel({ - extensions: ['.js', '.ts'], + extensions: ['.ts'], babelHelpers: 'bundled', exclude: 'node_modules/**', presets: ['@babel/preset-typescript'], diff --git a/src/index.ts b/src/index.ts index 87a0d9a..20b5dad 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,9 +3,17 @@ */ import getPath from './get-path'; -type MatcherFn = (node: Element) => T | undefined; -type MatcherObj = { [key: string]: MatcherFn | MatcherObj }; -type Matcher = MatcherFn | MatcherObj; +export type MatcherFn = (node: Element) => T | undefined; + +export type MatcherObj = { [key: string]: MatcherFn | MatcherObj }; + +export type MatcherObjResult = { + [K in keyof O]: O[K] extends F + ? ReturnType + : O[K] extends MatcherObj + ? MatcherObjResult + : never; +}; /** * Function returning a DOM document created by `createHTMLDocument`. The same @@ -28,21 +36,56 @@ const getDocument = (() => { * Given a markup string or DOM element, creates an object aligning with the * shape of the matchers object, or the value returned by the matcher. * - * @param source Source content - * @param matchers Matcher function or object of matchers - * @return Matched value(s), shaped by object + * @param source Source content + * @param matchers Object of matchers + * @return Matched values, shaped by object */ -export function parse, O extends MatcherObj = {}>( - source: Element | string, +export function parse( + source: string | Element, matchers: O -): { [K in keyof O]: O[K] extends F ? ReturnType : O[K] }; -export function parse>( - source: Element | string, - matchers: F -): ReturnType; -export function parse, O extends MatcherObj = {}>( - source: Element | string, +): MatcherObjResult; + +/** + * Given a markup string or DOM element, creates an object aligning with the + * shape of the matchers object, or the value returned by the matcher. + * + * @param source Source content + * @param matcher Matcher function + * @return Matched value + */ +export function parse(source: string | Element, matchers: F): ReturnType; + +/** + * Given a markup string or DOM element, creates an object aligning with the + * shape of the matchers object, or the value returned by the matcher. + * + * @param source Source content + * @param matchers Matcher function or object of matchers + */ +export function parse( + source: string | Element, matchers: O | F +): MatcherObjResult | ReturnType; + +/** + * Given a markup string or DOM element, creates an object aligning with the + * shape of the matchers object, or the value returned by the matcher. + * + * @param source Source content + * @param matchers Matcher function or object of matchers + */ +export function parse(source: string | Element, matchers?: undefined): undefined; + +/** + * Given a markup string or DOM element, creates an object aligning with the + * shape of the matchers object, or the value returned by the matcher. + * + * @param source Source content + * @param matchers Matcher function or object of matchers + */ +export function parse( + source: string | Element, + matchers?: O | F ) { if (!matchers) { return; @@ -56,7 +99,7 @@ export function parse, O extends MatcherObj = {}>( } // Return singular value - if ('function' === typeof matchers) { + if (typeof matchers === 'function') { return matchers(source); } @@ -66,24 +109,50 @@ export function parse, O extends MatcherObj = {}>( } // Shape result by matcher object - return Object.keys(matchers).reduce((memo, key) => { - memo[key] = parse(source, matchers[key] as MatcherObj); + return Object.keys(matchers).reduce((memo, key: keyof MatcherObjResult) => { + const inner = matchers[key]; + memo[key] = parse(source, inner); return memo; - }, {} as Record); + }, {} as MatcherObjResult); } +/** + * Generates a function which matches node of type selector, returning an + * attribute by property if the attribute exists. If no selector is passed, + * returns property of the query element. + * + * @param name Property name + * @return Property value + */ +export function prop(name: string): MatcherFn; + +/** + * Generates a function which matches node of type selector, returning an + * attribute by property if the attribute exists. If no selector is passed, + * returns property of the query element. + * + * @param selector Optional selector + * @param name Property name + * @return Property value + */ +export function prop( + selector: string | undefined, + name: string +): MatcherFn; + /** * Generates a function which matches node of type selector, returning an * attribute by property if the attribute exists. If no selector is passed, * returns property of the query element. * * @param selector Optional selector - * @param name Property name - * @return Property value + * @param name Property name + * @return Property value */ -export function prop(name: string): MatcherFn; -export function prop(selector: string | undefined, name: string): MatcherFn; -export function prop(arg1: string | undefined, arg2?: string): MatcherFn { +export function prop( + arg1: string | undefined, + arg2?: string +): MatcherFn { let name: string; let selector: string | undefined; if (1 === arguments.length) { @@ -93,29 +162,48 @@ export function prop(arg1: string | undefined, arg2?: string): MatcherF name = arg2 as string; selector = arg1; } - return function (node: Element) { + return function (node: E): E[K] | undefined { let match: Element | null = node; if (selector) { match = node.querySelector(selector); } if (match) { - return getPath(match, name) as T; + return getPath(match, name) as E[K] | undefined; } - }; + } as MatcherFn; } +/** + * Generates a function which matches node of type selector, returning an + * attribute by name if the attribute exists. If no selector is passed, + * returns attribute of the query element. + * + * @param name Attribute name + * @return Attribute value + */ +export function attr(name: string): MatcherFn; + /** * Generates a function which matches node of type selector, returning an * attribute by name if the attribute exists. If no selector is passed, * returns attribute of the query element. * * @param selector Optional selector - * @param name Attribute name - * @return Attribute value + * @param name Attribute name + * @return Attribute value */ -export function attr(name: string): MatcherFn; -export function attr(selector: string | undefined, name: string): MatcherFn; -export function attr(arg1: string | undefined, arg2?: string): MatcherFn { +export function attr(selector: string | undefined, name: string): MatcherFn; + +/** + * Generates a function which matches node of type selector, returning an + * attribute by name if the attribute exists. If no selector is passed, + * returns attribute of the query element. + * + * @param selector Optional selector + * @param name Attribute name + * @return Attribute value + */ +export function attr(arg1: string | undefined, arg2?: string): MatcherFn { let name: string; let selector: string | undefined; if (1 === arguments.length) { @@ -125,10 +213,10 @@ export function attr(arg1: string | undefined, arg2?: string): MatcherF name = arg2 as string; selector = arg1; } - return function (node: Element): T | undefined { - const attributes = prop(selector, 'attributes')(node); + return function (node: Element): string | undefined { + const attributes = prop(selector, 'attributes')(node) as NamedNodeMap | undefined; if (attributes && Object.prototype.hasOwnProperty.call(attributes, name)) { - return attributes[name].value; + return attributes[name as any].value; } }; } @@ -139,10 +227,10 @@ export function attr(arg1: string | undefined, arg2?: string): MatcherF * @see prop() * * @param selector Optional selector - * @return Inner HTML + * @return Inner HTML */ -export function html(selector?: string) { - return prop(selector, 'innerHTML'); +export function html(selector?: string) { + return prop(selector, 'innerHTML'); } /** @@ -150,11 +238,11 @@ export function html(selector?: string) { * * @see prop() * - * @param selector Optional selector - * @return Text content + * @param selector Optional selector + * @return Text content */ -export function text(selector?: string) { - return prop(selector, 'textContent'); +export function text(selector?: string) { + return prop(selector, 'textContent'); } /** @@ -168,9 +256,9 @@ export function text(selector?: string) { * @param matchers Matcher function or object of matchers * @return Matcher function which returns an array of matched value(s) */ -export function query(selector: string, matchers: Matcher) { +export function query(selector: string, matchers: MatcherObj) { return function (node: Element): any[] { const matches = node.querySelectorAll(selector); - return [].map.call(matches, (match) => parse>(match, matchers)); + return [].map.call(matches, (match) => parse(match, matchers)); }; } diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json deleted file mode 100644 index ff08772..0000000 --- a/tsconfig.eslint.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "./tsconfig", - "compilerOptions": { - "noEmit": true - }, - "include": ["**/*.js"] -}