diff --git a/packages/kg-clean-basic-html/.gitignore b/packages/kg-clean-basic-html/.gitignore index 8e6861d207..dceaa14a65 100644 --- a/packages/kg-clean-basic-html/.gitignore +++ b/packages/kg-clean-basic-html/.gitignore @@ -1,2 +1,4 @@ +build/ cjs/ es/ +tsconfig.tsbuildinfo diff --git a/packages/kg-clean-basic-html/eslint.config.mjs b/packages/kg-clean-basic-html/eslint.config.mjs index e41bea6d80..a3e54db18a 100644 --- a/packages/kg-clean-basic-html/eslint.config.mjs +++ b/packages/kg-clean-basic-html/eslint.config.mjs @@ -1,45 +1,33 @@ -import {fixupPluginRules} from '@eslint/compat'; +import {defineConfig} from 'eslint/config'; import eslint from '@eslint/js'; import ghostPlugin from 'eslint-plugin-ghost'; -import globals from 'globals'; +import tseslint from 'typescript-eslint'; -const ghost = fixupPluginRules(ghostPlugin); - -export default [ - {ignores: ['build/**', 'cjs/**', 'es/**']}, - eslint.configs.recommended, - { - files: ['**/*.js'], - plugins: {ghost}, - languageOptions: { - globals: { - ...globals.node, - ...globals.browser - } - }, - rules: { - ...ghostPlugin.configs.node.rules, - // match ESLint 8 behavior for catch clause variables - 'no-unused-vars': ['error', {caughtErrors: 'none'}], - // disable rules incompatible with ESLint 9 flat config - 'ghost/filenames/match-exported-class': 'off', - 'ghost/filenames/match-exported': 'off', - 'ghost/filenames/match-regex': 'off' - } +export default defineConfig([ + { ignores: ['build/**'] }, + { + files: ['**/*.ts'], + extends: [ + eslint.configs.recommended, + tseslint.configs.recommended, + ], + languageOptions: { + parserOptions: { ecmaVersion: 2022, sourceType: 'module' }, + }, + plugins: { ghost: ghostPlugin }, + rules: { + ...ghostPlugin.configs.ts.rules, + '@typescript-eslint/no-explicit-any': 'error', + }, + }, + { + files: ['test/**/*.ts'], + rules: { + ...ghostPlugin.configs['ts-test'].rules, + 'ghost/mocha/no-global-tests': 'off', + 'ghost/mocha/handle-done-callback': 'off', + 'ghost/mocha/no-mocha-arrows': 'off', + 'ghost/mocha/max-top-level-suites': 'off', }, - { - files: ['test/**/*.js'], - plugins: {ghost}, - languageOptions: { - globals: { - ...globals.node, - ...globals.mocha, - should: true, - sinon: true - } - }, - rules: { - ...ghostPlugin.configs.test.rules - } - } -]; + }, +]); diff --git a/packages/kg-clean-basic-html/package.json b/packages/kg-clean-basic-html/package.json index 11b4d66525..62fa879d6e 100644 --- a/packages/kg-clean-basic-html/package.json +++ b/packages/kg-clean-basic-html/package.json @@ -1,18 +1,25 @@ { "name": "@tryghost/kg-clean-basic-html", "version": "4.2.20", - "repository": "https://github.com/TryGhost/Koenig/tree/master/packages/kg-clean-basic-html", + "repository": "https://github.com/TryGhost/Koenig/tree/main/packages/kg-clean-basic-html", "author": "Ghost Foundation", "license": "MIT", - "main": "cjs/clean-basic-html.js", - "module": "es/clean-basic-html.js", - "source": "lib/clean-basic-html.js", + "main": "build/cjs/index.js", + "module": "build/esm/index.js", + "types": "build/esm/index.d.ts", + "exports": { + ".": { + "types": "./build/esm/index.d.ts", + "import": "./build/esm/index.js", + "require": "./build/cjs/index.js" + } + }, "scripts": { - "dev": "echo \"Implement me!\"", - "build": "rollup -c", - "prepare": "NODE_ENV=production yarn build", - "pretest": "yarn build", - "test": "NODE_ENV=testing c8 --all --reporter text --reporter cobertura mocha './test/**/*.test.js'", + "dev": "tsc --watch --preserveWatchOutput", + "build": "tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"module\"}' > build/esm/package.json", + "prepare": "tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"module\"}' > build/esm/package.json", + "pretest": "tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"module\"}' > build/esm/package.json && tsc -p tsconfig.test.json", + "test": "NODE_ENV=testing c8 --all --reporter text --reporter cobertura tsx node_modules/.bin/mocha './test/**/*.test.ts'", "lint": "eslint . --cache", "posttest": "yarn lint" }, @@ -20,24 +27,23 @@ "node": "^22.13.1 || ^24.0.0" }, "files": [ - "LICENSE", - "README.md", - "cjs/", - "es/", - "lib/" + "build" ], "publishConfig": { "access": "public" }, "devDependencies": { - "@babel/core": "7.29.0", - "@babel/preset-env": "7.29.0", - "@rollup/plugin-babel": "7.0.0", + "@eslint/js": "9.39.4", + "@types/mocha": "^10.0.0", + "@types/node": "^22.0.0", + "@types/sinon": "^17.0.0", "c8": "11.0.0", "jsdom": "29.0.0", "mocha": "11.7.5", - "rollup": "4.59.0", "should": "13.2.3", - "sinon": "21.0.2" + "sinon": "21.0.2", + "tsx": "^4.0.0", + "typescript": "5.9.3", + "typescript-eslint": "8.33.1" } } diff --git a/packages/kg-clean-basic-html/rollup.config.mjs b/packages/kg-clean-basic-html/rollup.config.mjs deleted file mode 100644 index 8b119f5e3c..0000000000 --- a/packages/kg-clean-basic-html/rollup.config.mjs +++ /dev/null @@ -1,43 +0,0 @@ -/* eslint-env node */ -import pkg from './package.json' with { type: 'json' }; -import babel from '@rollup/plugin-babel'; - -export default [ - // Node build. - // No transpilation or bundling other than conversion from es modules to cjs - { - input: pkg.source, - output: { - file: pkg.main, - format: 'cjs', - interop: false - } - }, - - // ES module build - // Transpiles to target browser support for use in client apps - { - input: pkg.source, - output: { - file: pkg.module, - format: 'es', - sourcemap: true - }, - plugins: [ - babel({ - babelHelpers: 'bundled', - presets: [ - ['@babel/preset-env', { - modules: false, - targets: [ - 'last 2 Chrome versions', - 'last 2 Firefox versions', - 'last 2 Safari versions' - ].join(', ') - }] - ], - exclude: ['node_modules/**', '../../node_modules/**'] - }) - ] - } -]; diff --git a/packages/kg-clean-basic-html/lib/clean-basic-html.js b/packages/kg-clean-basic-html/src/clean-basic-html.ts similarity index 63% rename from packages/kg-clean-basic-html/lib/clean-basic-html.js rename to packages/kg-clean-basic-html/src/clean-basic-html.ts index 8139246830..78110705a4 100644 --- a/packages/kg-clean-basic-html/lib/clean-basic-html.js +++ b/packages/kg-clean-basic-html/src/clean-basic-html.ts @@ -1,42 +1,32 @@ -/** - * Removes any wrappers around replacement strings {foo} - * Example input: {foo} - * Example output: {foo} - * @param {string} html - * @returns {string} - */ -function removeCodeWrappers(html) { +interface CleanBasicHtmlOptions { + allowBr?: boolean; + firstChildInnerContent?: boolean; + removeCodeWrappers?: boolean; + createDocument?: (html: string) => Document; +} + +function removeCodeWrappers(html: string): string { return html.replace(/]*>((.*?){.*?}(.*?))<\/code>/gi, '$1'); } -/** - * Parses an html string and returns a cleaned version - * @param {string} html - * @param {Object} _options - * @param {boolean} [_options.allowBr] - if true,
tags will be kept - * @param {boolean} [_options.firstChildInnerContent] - if true, only the innerHTML of the first element will be returned - * @param {boolean} [_options.removeCodeWrappers] - if true, wrappers around replacement strings {foo} will be removed - * @returns {string} - */ -export default function cleanBasicHtml(html = '', _options = {}) { +export default function cleanBasicHtml(html: string = '', _options: CleanBasicHtmlOptions = {}): string | null { const defaults = {}; - const options = Object.assign({}, defaults, _options); + const options: CleanBasicHtmlOptions = Object.assign({}, defaults, _options); if (!options.createDocument) { const Parser = (typeof DOMParser !== 'undefined' && DOMParser) || (typeof window !== 'undefined' && window.DOMParser); if (!Parser) { - // eslint-disable-next-line ghost/ghost-custom/no-native-error throw new Error('cleanBasicHtml() must be passed a `createDocument` function as an option when used in a non-browser environment'); } - options.createDocument = function (docHtml) { + options.createDocument = function (docHtml: string): Document { const parser = new Parser(); return parser.parseFromString(docHtml, 'text/html'); }; } - let cleanHtml = html; + let cleanHtml: string = html; if (!options.allowBr || cleanHtml === '
') { cleanHtml = cleanHtml @@ -55,7 +45,7 @@ export default function cleanBasicHtml(html = '', _options = {}) { // remove any elements that have a blank textContent if (cleanHtml) { - let doc = options.createDocument(cleanHtml); + const doc = options.createDocument(cleanHtml); // don't analyze the document if it's empty (can result in storing
tags if allowed) if (doc.body.textContent === '') { @@ -64,7 +54,7 @@ export default function cleanBasicHtml(html = '', _options = {}) { doc.body.querySelectorAll('*').forEach((element) => { // Treat Zero Width Non-Joiner characters as spaces - if (!element.textContent.trim().replace(/\u200c+/g, '')) { + if (!element.textContent?.trim().replace(/\u200c+/g, '')) { if (options.allowBr && element.tagName === 'BR') { // keep it return; @@ -72,9 +62,9 @@ export default function cleanBasicHtml(html = '', _options = {}) { if (options.allowBr && element.querySelector('br')) { return element.replaceWith(doc.createElement('br')); } - if (element.textContent.length > 0) { + if (element.textContent && element.textContent.length > 0) { // keep a single space to avoid collapsing spaces - let space = doc.createTextNode(' '); + const space = doc.createTextNode(' '); return element.replaceWith(space); } return element.remove(); diff --git a/packages/kg-clean-basic-html/src/index.ts b/packages/kg-clean-basic-html/src/index.ts new file mode 100644 index 0000000000..400c8e18ee --- /dev/null +++ b/packages/kg-clean-basic-html/src/index.ts @@ -0,0 +1 @@ +export {default} from './clean-basic-html.js'; diff --git a/packages/kg-clean-basic-html/test/clean-basic-html.test.js b/packages/kg-clean-basic-html/test/clean-basic-html.test.ts similarity index 74% rename from packages/kg-clean-basic-html/test/clean-basic-html.test.js rename to packages/kg-clean-basic-html/test/clean-basic-html.test.ts index 81c46370fb..8219014bf3 100644 --- a/packages/kg-clean-basic-html/test/clean-basic-html.test.js +++ b/packages/kg-clean-basic-html/test/clean-basic-html.test.ts @@ -1,16 +1,17 @@ // Switch these lines once there are useful utils -// const testUtils = require('./utils'); -require('./utils'); +// import testUtils from './utils/index.js'; +import './utils/index.js'; -const {JSDOM} = require('jsdom'); -const cleanBasicHtml = require('../'); +import should from 'should'; +import {JSDOM} from 'jsdom'; +import cleanBasicHtml from '../src/clean-basic-html.js'; describe('cleanBasicHtml', function () { - let options = {}; + let options: {createDocument: (html: string) => Document}; before(function () { options = { - createDocument(html) { + createDocument(html: string) { return (new JSDOM(html)).window.document; } }; @@ -26,42 +27,42 @@ describe('cleanBasicHtml', function () { const html = '
    '; const result = cleanBasicHtml(html, options); - result.should.equal(''); + (result as string).should.equal(''); }); it('keeps whitespace between text', function () { const html = ' 
Testing  Significant Whitespace
 '; const result = cleanBasicHtml(html, options); - result.should.equal('Testing Significant Whitespace'); + (result as string).should.equal('Testing Significant Whitespace'); }); it('removes DOM elements with blank text content', function () { const html = ' 

  

'; const result = cleanBasicHtml(html, options); - result.should.equal(''); + (result as string).should.equal(''); }); it('keeps elements with text content', function () { const html = '   Test  '; const result = cleanBasicHtml(html, options); - result.should.equal(' Test '); + (result as string).should.equal(' Test '); }); it('can extract first element content', function () { const html = '

Headline italic

'; const result = cleanBasicHtml(html, {...options, firstChildInnerContent: true}); - result.should.equal('Headline italic'); + (result as string).should.equal('Headline italic'); }); it('return empty string if firstChildInnerContent option enabled and there is no first child element ', function () { const html = ''; const result = cleanBasicHtml(html, {...options, firstChildInnerContent: true}); - result.should.equal(''); + (result as string).should.equal(''); }); describe('options.removeCodeWrappers', function () { @@ -69,21 +70,21 @@ describe('cleanBasicHtml', function () { const html = '{foo}'; const result = cleanBasicHtml(html, {...options, removeCodeWrappers: true}); - result.should.equal('{foo}'); + (result as string).should.equal('{foo}'); }); it('removes any wrappers around replacement strings {foo}', function () { const html = '{foo}'; const result = cleanBasicHtml(html, {...options, removeCodeWrappers: true}); - result.should.equal('{foo}'); + (result as string).should.equal('{foo}'); }); it('removes any wrappers around replacement strings {foo, "default"}', function () { const html = '

Hey {first_name, "there"},

'; const result = cleanBasicHtml(html, {...options, removeCodeWrappers: true}); - result.should.equal('

Hey {first_name, "there"},

'); + (result as string).should.equal('

Hey {first_name, "there"},

'); }); }); }); diff --git a/packages/kg-clean-basic-html/test/utils/assertions.js b/packages/kg-clean-basic-html/test/utils/assertions.ts similarity index 100% rename from packages/kg-clean-basic-html/test/utils/assertions.js rename to packages/kg-clean-basic-html/test/utils/assertions.ts diff --git a/packages/kg-clean-basic-html/test/utils/index.js b/packages/kg-clean-basic-html/test/utils/index.ts similarity index 76% rename from packages/kg-clean-basic-html/test/utils/index.js rename to packages/kg-clean-basic-html/test/utils/index.ts index 0d67d86ff8..f385b7bf56 100644 --- a/packages/kg-clean-basic-html/test/utils/index.js +++ b/packages/kg-clean-basic-html/test/utils/index.ts @@ -5,7 +5,7 @@ */ // Require overrides - these add globals for tests -require('./overrides'); +import './overrides.js'; // Require assertions - adds custom should assertions -require('./assertions'); +import './assertions.js'; diff --git a/packages/kg-clean-basic-html/test/utils/overrides.js b/packages/kg-clean-basic-html/test/utils/overrides.js deleted file mode 100644 index 90203424ee..0000000000 --- a/packages/kg-clean-basic-html/test/utils/overrides.js +++ /dev/null @@ -1,10 +0,0 @@ -// This file is required before any test is run - -// Taken from the should wiki, this is how to make should global -// Should is a global in our eslint test config -global.should = require('should').noConflict(); -should.extend(); - -// Sinon is a simple case -// Sinon is a global in our eslint test config -global.sinon = require('sinon'); diff --git a/packages/kg-clean-basic-html/test/utils/overrides.ts b/packages/kg-clean-basic-html/test/utils/overrides.ts new file mode 100644 index 0000000000..6138e929ff --- /dev/null +++ b/packages/kg-clean-basic-html/test/utils/overrides.ts @@ -0,0 +1,15 @@ +// This file is required before any test is run + +// Taken from the should wiki, this is how to make should global +// Should is a global in our eslint test config +import should from 'should'; +import sinon from 'sinon'; + +// @types/should is incomplete — noConflict and extend exist at runtime +const shouldModule = should as unknown as {noConflict(): typeof should; extend(): void}; +Object.defineProperty(globalThis, 'should', {value: shouldModule.noConflict(), writable: true, configurable: true}); +shouldModule.extend(); + +// Sinon is a simple case +// Sinon is a global in our eslint test config +Object.defineProperty(globalThis, 'sinon', {value: sinon, writable: true, configurable: true}); diff --git a/packages/kg-clean-basic-html/tsconfig.cjs.json b/packages/kg-clean-basic-html/tsconfig.cjs.json new file mode 100644 index 0000000000..bd981491bc --- /dev/null +++ b/packages/kg-clean-basic-html/tsconfig.cjs.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "build/cjs", + "verbatimModuleSyntax": false, + "declaration": false, + "declarationMap": false, + "sourceMap": false, + "incremental": false + } +} diff --git a/packages/kg-clean-basic-html/tsconfig.json b/packages/kg-clean-basic-html/tsconfig.json new file mode 100644 index 0000000000..56c51dd045 --- /dev/null +++ b/packages/kg-clean-basic-html/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "ES2022", + "moduleResolution": "bundler", + "rootDir": "src", + "outDir": "build/esm", + "strict": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "incremental": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true + }, + "include": ["src/**/*"] +} diff --git a/packages/kg-clean-basic-html/tsconfig.test.json b/packages/kg-clean-basic-html/tsconfig.test.json new file mode 100644 index 0000000000..b092804ad8 --- /dev/null +++ b/packages/kg-clean-basic-html/tsconfig.test.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "outDir": null, + "noEmit": true, + "declaration": false, + "declarationMap": false, + "sourceMap": false, + "incremental": false + }, + "include": [ + "src/**/*", + "test/**/*" + ] +}