Skip to content

Commit 3d7be43

Browse files
committed
Migrate kg-lexical-html-renderer to TypeScript
- Move lib/ to src/, update tsconfig rootDir - Update tsconfig.json: NodeNext module, verbatimModuleSyntax, declarationMap - Add "type": "module" to package.json - Convert source from CJS to ESM (require -> import, module.exports -> export) - Add .js extensions to all relative imports - Rename test files .js to .ts with type annotations - Switch test runner to tsx - Replace .eslintrc.js with eslint.config.js (flat config)
1 parent 16bbc11 commit 3d7be43

34 files changed

Lines changed: 351 additions & 213 deletions

packages/kg-default-nodes/src/KoenigDecoratorNode.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* c8 ignore start */
22
import {DecoratorNode} from 'lexical';
3+
import type {ExportDOMOptions, ExportDOMOutput} from './export-dom.js';
34

45
export class KoenigDecoratorNode extends DecoratorNode<unknown> {
56
static transform() {
@@ -11,7 +12,19 @@ export class KoenigDecoratorNode extends DecoratorNode<unknown> {
1112
}
1213
}
1314

14-
export function $isKoenigCard(node: unknown): node is KoenigDecoratorNode {
15+
// A Ghost card at runtime is a KoenigDecoratorNode plus a custom exportDOM
16+
// signature and optional dynamic-data hooks. We can't declare these on the
17+
// class itself — Lexical's DecoratorNode.exportDOM has an incompatible
18+
// signature and the override (see generate-decorator-node.ts) relies on a
19+
// localised @ts-expect-error. This intersection type is what $isKoenigCard
20+
// narrows to so consumers get the card-shaped surface.
21+
export type KoenigCard = KoenigDecoratorNode & {
22+
exportDOM(options: ExportDOMOptions): ExportDOMOutput<Element>;
23+
hasDynamicData?(): boolean;
24+
getDynamicData?(options: ExportDOMOptions): Promise<{key: number; data: unknown}>;
25+
};
26+
27+
export function $isKoenigCard(node: unknown): node is KoenigCard {
1528
return node instanceof KoenigDecoratorNode;
1629
}
1730
/* c8 ignore end */
Lines changed: 13 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,33 @@
1-
import {fixupPluginRules} from '@eslint/compat';
21
import eslint from '@eslint/js';
2+
import {defineConfig} from 'eslint/config';
33
import ghostPlugin from 'eslint-plugin-ghost';
4-
import globals from 'globals';
54
import tseslint from 'typescript-eslint';
65

7-
const ghost = fixupPluginRules(ghostPlugin);
8-
9-
export default tseslint.config(
10-
{ignores: ['build/**', 'cjs/**', 'es/**']},
6+
export default defineConfig([
7+
{ignores: ['build/**']},
118
{
12-
files: ['lib/**/*.ts'],
9+
files: ['**/*.ts'],
1310
extends: [
1411
eslint.configs.recommended,
1512
tseslint.configs.recommended
1613
],
17-
plugins: {ghost},
1814
languageOptions: {
19-
globals: globals.node
15+
parserOptions: {ecmaVersion: 2022, sourceType: 'module'}
2016
},
17+
plugins: {ghost: ghostPlugin},
2118
rules: {
2219
...ghostPlugin.configs.ts.rules,
23-
'ghost/filenames/match-exported-class': 'off',
24-
'@typescript-eslint/no-unused-expressions': 'off',
25-
'@typescript-eslint/no-require-imports': 'off'
20+
'@typescript-eslint/no-explicit-any': 'error'
2621
}
2722
},
2823
{
29-
files: ['test/**/*.js'],
30-
extends: [
31-
eslint.configs.recommended
32-
],
33-
plugins: {ghost},
34-
languageOptions: {
35-
globals: {
36-
...globals.node,
37-
...globals.mocha,
38-
should: true,
39-
sinon: true
40-
}
41-
},
24+
files: ['test/**/*.ts'],
4225
rules: {
43-
...ghostPlugin.configs.node.rules,
44-
'no-unused-vars': ['error', {caughtErrors: 'none'}],
45-
'ghost/filenames/match-exported-class': 'off',
46-
'ghost/filenames/match-exported': 'off',
47-
'ghost/filenames/match-regex': 'off',
48-
...ghostPlugin.configs.test.rules,
26+
...ghostPlugin.configs['ts-test'].rules,
27+
'ghost/mocha/no-global-tests': 'off',
28+
'ghost/mocha/handle-done-callback': 'off',
29+
'ghost/mocha/no-mocha-arrows': 'off',
4930
'ghost/mocha/max-top-level-suites': 'off'
5031
}
5132
}
52-
);
33+
]);

packages/kg-lexical-html-renderer/lib/index.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

packages/kg-lexical-html-renderer/lib/transformers/index.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.

packages/kg-lexical-html-renderer/package.json

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,25 @@
44
"repository": "https://github.com/TryGhost/Koenig/tree/main/packages/kg-lexical-html-renderer",
55
"author": "Ghost Foundation",
66
"license": "MIT",
7-
"main": "build/index.js",
8-
"types": "build/index.d.ts",
7+
"main": "build/cjs/index.js",
8+
"module": "build/esm/index.js",
9+
"types": "build/esm/index.d.ts",
10+
"exports": {
11+
".": {
12+
"types": "./build/esm/index.d.ts",
13+
"import": "./build/esm/index.js",
14+
"require": "./build/cjs/index.js"
15+
}
16+
},
917
"scripts": {
1018
"dev": "tsc --watch --preserveWatchOutput --sourceMap",
11-
"build": "tsc",
12-
"prepare": "tsc",
13-
"pretest": "yarn build",
19+
"build": "tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"module\"}' > build/esm/package.json",
20+
"prepare": "tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"module\"}' > build/esm/package.json",
21+
"pretest": "tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"module\"}' > build/esm/package.json && tsc -p tsconfig.test.json",
1422
"test": "yarn test:unit && yarn test:types",
15-
"test:unit": "NODE_ENV=testing c8 --lib --check-coverage --reporter text --reporter cobertura mocha './test/**/*.test.js'",
23+
"test:unit": "NODE_ENV=testing c8 --src src --all --check-coverage --reporter text --reporter cobertura mocha --require tsx './test/**/*.test.ts'",
1624
"test:types": "tsc --noEmit",
17-
"lint": "eslint . --cache",
25+
"lint": "eslint",
1826
"posttest": "yarn lint"
1927
},
2028
"files": [
@@ -26,12 +34,20 @@
2634
"access": "public"
2735
},
2836
"devDependencies": {
37+
"@eslint/js": "9.39.4",
38+
"@types/jsdom": "21.1.7",
39+
"@types/mocha": "10.0.10",
40+
"@types/should": "13.0.0",
41+
"@types/sinon": "21.0.0",
2942
"c8": "11.0.0",
3043
"jsdom": "29.0.2",
3144
"mocha": "11.7.5",
3245
"prettier": "3.8.2",
3346
"should": "13.2.3",
34-
"sinon": "21.1.2"
47+
"sinon": "21.1.2",
48+
"tsx": "4.21.0",
49+
"typescript": "5.9.3",
50+
"typescript-eslint": "8.57.0"
3551
},
3652
"dependencies": {
3753
"@lexical/clipboard": "0.13.1",

packages/kg-lexical-html-renderer/src/LexicalHTMLRenderer.ts

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,35 @@
1-
import {SerializedEditorState, LexicalEditor, LexicalNode, Klass} from 'lexical';
21
import {createHeadlessEditor} from '@lexical/headless';
32
import {ListItemNode, ListNode} from '@lexical/list';
43
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
54
import {LinkNode} from '@lexical/link';
6-
import $convertToHtmlString from './convert-to-html-string';
7-
import getDynamicDataNodes from './get-dynamic-data-nodes';
8-
9-
// TODO: Using import causes circular definitions for kg-default-nodes
10-
11-
const {registerRemoveAtLinkNodesTransform} = require('@tryghost/kg-default-transforms');
5+
import {JSDOM} from 'jsdom';
6+
import $convertToHtmlString from './convert-to-html-string.js';
7+
import getDynamicDataNodes from './get-dynamic-data-nodes.js';
8+
import {registerRemoveAtLinkNodesTransform} from '@tryghost/kg-default-transforms';
9+
import type {SerializedEditorState, LexicalEditor, LexicalNode, Klass} from 'lexical';
10+
import type {ExportDOMDom} from '@tryghost/kg-default-nodes';
11+
import type {RendererOptions} from './types.js';
1212

1313
interface RenderOptions {
1414
target?: 'html' | 'email' | 'plaintext';
15-
dom?: import('jsdom').JSDOM;
15+
dom?: ExportDOMDom;
1616
// TODO: we should define some standard here once we move to more cards with dynamic data
17-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
18-
renderData?: Map<number, any>;
17+
renderData?: Map<number, unknown>;
1918
}
2019

2120
function defaultOnError() {
2221
// do nothing
2322
}
2423

2524
export default class LexicalHTMLRenderer {
26-
dom: import('jsdom').JSDOM;
25+
dom: ExportDOMDom;
2726
nodes: Klass<LexicalNode>[];
2827
onError: (error: Error) => void;
2928

30-
constructor({dom, nodes, onError}: {dom?: import('jsdom').JSDOM, nodes?: Klass<LexicalNode>[], onError?: () => void} = {}) {
31-
if (!dom) {
32-
const jsdom = require('jsdom');
33-
const {JSDOM} = jsdom;
34-
35-
this.dom = new JSDOM();
36-
} else {
37-
this.dom = dom;
38-
}
29+
constructor({dom, nodes, onError}: {dom?: ExportDOMDom, nodes?: Klass<LexicalNode>[], onError?: () => void} = {}) {
30+
// JSDOM default is a Node-side convenience. Consumers in browser
31+
// environments can pass any {window: {document}}-shaped object.
32+
this.dom = dom ?? new JSDOM();
3933

4034
this.nodes = nodes || [];
4135
this.onError = onError || defaultOnError;
@@ -46,7 +40,7 @@ export default class LexicalHTMLRenderer {
4640
target: 'html',
4741
dom: this.dom
4842
};
49-
const options = Object.assign({}, defaultOptions, userOptions);
43+
const options: RendererOptions = Object.assign({}, defaultOptions, userOptions) as RendererOptions;
5044

5145
const DEFAULT_NODES: Array<Klass<LexicalNode>> = [
5246
HeadingNode,

packages/kg-lexical-html-renderer/src/convert-to-html-string.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import {ElementNode, LexicalNode} from 'lexical';
21
import {$getRoot, $isElementNode, $isLineBreakNode, $isParagraphNode, $isTextNode} from 'lexical';
32
import {$isLinkNode} from '@lexical/link';
4-
import {$isKoenigCard, RendererOptions} from '@tryghost/kg-default-nodes';
5-
import TextContent from './utils/TextContent';
6-
import elementTransformers from './transformers';
3+
import {$isKoenigCard} from '@tryghost/kg-default-nodes';
4+
import TextContent from './utils/TextContent.js';
5+
import elementTransformers from './transformers/index.js';
6+
import type {ElementNode, LexicalNode} from 'lexical';
7+
import type {RendererOptions} from './types.js';
78

89
export default function $convertToHtmlString(options: RendererOptions = {}): string {
910
const output: string[] = [];
@@ -31,15 +32,13 @@ export default function $convertToHtmlString(options: RendererOptions = {}): str
3132

3233
function exportTopLevelElementOrDecorator(node: LexicalNode, options: RendererOptions): string | null {
3334
if ($isKoenigCard(node)) {
34-
// NOTE: kg-default-nodes appends type in rare cases to make use of this functionality... with moving to typescript,
35-
// we should change this implementation because it's confusing, or we should override the DOMExportOutput type
3635
const {element, type} = node.exportDOM(options);
3736

3837
switch (type) {
3938
case 'inner':
4039
return element.innerHTML;
4140
case 'value':
42-
if ('value' in element) {
41+
if ('value' in element && typeof element.value === 'string') {
4342
return element.value;
4443
}
4544

packages/kg-lexical-html-renderer/src/get-dynamic-data-nodes.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import {$getRoot} from 'lexical';
2-
import {$isKoenigCard, KoenigDecoratorNode} from '@tryghost/kg-default-nodes';
3-
2+
import {$isKoenigCard} from '@tryghost/kg-default-nodes';
3+
import type {KoenigCard} from '@tryghost/kg-default-nodes';
44
import type {EditorState} from 'lexical';
55

6-
export default function getDynamicDataNodes(editorState: EditorState): KoenigDecoratorNode[] {
7-
const dynamicNodes: KoenigDecoratorNode[] = [];
6+
export default function getDynamicDataNodes(editorState: EditorState): KoenigCard[] {
7+
const dynamicNodes: KoenigCard[] = [];
88

99
editorState.read(() => {
1010
const root = $getRoot();
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/* c8 ignore start */
2+
import LexicalHTMLRenderer from './LexicalHTMLRenderer.js';
3+
4+
export {LexicalHTMLRenderer};
5+
/* c8 ignore stop */

packages/kg-lexical-html-renderer/src/kg-default-nodes.d.ts

Lines changed: 0 additions & 28 deletions
This file was deleted.

0 commit comments

Comments
 (0)