Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrated kg-default-nodes to typescript #1158

Open
wants to merge 33 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
1ad8193
tsconfig setup
9larsons Feb 6, 2024
177295c
update c8 coverage
9larsons Feb 6, 2024
508e191
remove tsbuildinfo
9larsons Feb 6, 2024
2a4cad3
test converting a node
9larsons Feb 6, 2024
1206720
convert some nodes, need to resolve exportDOM
9larsons Feb 6, 2024
4a88ac0
patch up extended nodes
9larsons Feb 7, 2024
5e8a905
upd nodes
9larsons Feb 7, 2024
9482df1
update audio parser
9larsons Feb 7, 2024
1f4e49f
wire up generator
9larsons Feb 7, 2024
4e78985
update generator class
9larsons Feb 7, 2024
79426de
update create audio node
9larsons Feb 7, 2024
6791f50
adjust dataset prop
9larsons Feb 7, 2024
3fb90f2
upd
9larsons Feb 7, 2024
bc47e1e
update linter
9larsons Feb 7, 2024
796fc50
drop to ts 5.2.0 to support ghost ts linter
9larsons Feb 7, 2024
35941db
revert constructor changes, weren't actually working
9larsons Feb 8, 2024
fda4b13
fix decorator node generation and dataset type
9larsons Feb 8, 2024
8822d9c
remove any
9larsons Feb 8, 2024
5e9a022
add notes
9larsons Feb 8, 2024
406cece
update nodes
9larsons Feb 8, 2024
f106a39
some parsers
9larsons Feb 8, 2024
841b752
update parsers to ts
9larsons Feb 12, 2024
b91817e
fix callout test
9larsons Feb 12, 2024
cf9c6ce
fix header
9larsons Feb 12, 2024
535b35a
update most utils
9larsons Feb 12, 2024
2b94bb2
update renderer options
9larsons Feb 12, 2024
84d673f
test exportDOM changes
9larsons Feb 13, 2024
f689b94
Updated lexical to v0.13.1 (#1079)
renovate[bot] Feb 6, 2024
7c7fe06
Published new versions
9larsons Feb 6, 2024
381be58
tsconfig setup
9larsons Feb 6, 2024
badd617
convert some nodes, need to resolve exportDOM
9larsons Feb 6, 2024
9071ccf
drop to ts 5.2.0 to support ghost ts linter
9larsons Feb 7, 2024
dce9f3f
bump image helpers
9larsons Mar 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 7 additions & 12 deletions packages/kg-default-nodes/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
module.exports = {
plugins: ['ghost'],
extends: [
'plugin:ghost/node'
'plugin:ghost/node',
'plugin:ghost/ts'
],
parser: '@babel/eslint-parser',
parserOptions: {
sourceType: 'module',
requireConfigFile: false,
babelOptions: {
plugins: [
'@babel/plugin-syntax-import-assertions'
]
}
},
env: {
browser: true,
node: true
},
// this fouls up with Lexical's need to prefix with '$' for lifecycle hooks
rules: {
'ghost/filenames/match-exported-class': 'off'
}
};
};
2 changes: 2 additions & 0 deletions packages/kg-default-nodes/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
cjs/
es/
build/
tsconfig.tsbuildinfo
11 changes: 0 additions & 11 deletions packages/kg-default-nodes/lib/KoenigDecoratorNode.js

This file was deleted.

12 changes: 12 additions & 0 deletions packages/kg-default-nodes/lib/KoenigDecoratorNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* eslint-disable ghost/filenames/match-exported-class */
/* c8 ignore start */
import {DecoratorNode} from 'lexical';
import type {LexicalNode} from 'lexical';

export class KoenigDecoratorNode extends DecoratorNode<JSX.Element> {
}

export function $isKoenigCard(node: LexicalNode): boolean {
return node instanceof KoenigDecoratorNode;
}
/* c8 ignore end */
Original file line number Diff line number Diff line change
@@ -1,55 +1,92 @@
import {NodeKey} from 'lexical';
import {KoenigDecoratorNode} from './KoenigDecoratorNode';
import readTextContent from './utils/read-text-content';
/**
* Validates the required arguments passed to `generateDecoratorNode`
* @see https://lexical.dev/docs/concepts/nodes#extending-decoratornode
*/
function validateArguments(nodeType, properties) {

// NOTE: There's some liberal use of 'any' in this file despite moving it to typescript. This is a difficult one to get right
// because we're generating classes.

export type KoenigDecoratorProperty = {
name: string;
default?: number | string | boolean | null | object;
urlType?: 'url'|'html'|'markdown';
wordCount?: boolean;
};

export type KoenigDecoratorRendererOutput = {
element: HTMLElement,
type?: 'inner' | 'value';
}

export type KoenigDecoratorNodeProperties = KoenigDecoratorProperty[];

function validateArguments(nodeType: string, properties: KoenigDecoratorNodeProperties) {
/* eslint-disable ghost/ghost-custom/no-native-error */
/* c8 ignore start */
if (!nodeType) {
throw new Error({message: '[generateDecoratorNode] A unique "nodeType" should be provided'});
throw new Error('[generateDecoratorNode] A unique "nodeType" should be provided');
}

properties.forEach((prop) => {
if (!('name' in prop) || !('default' in prop)){
throw new Error({message: '[generateDecoratorNode] Properties should have both "name" and "default" attributes.'});
throw new Error('[generateDecoratorNode] Properties should have both "name" and "default" attributes.');
}

if (prop.urlType && !['url', 'html', 'markdown'].includes(prop.urlType)) {
throw new Error({message: '[generateDecoratorNode] "urlType" should be either "url", "html" or "markdown"'});
throw new Error('[generateDecoratorNode] "urlType" should be either "url", "html" or "markdown"');
}

if ('wordCount' in prop && typeof prop.wordCount !== 'boolean') {
throw new Error({message: '[generateDecoratorNode] "wordCount" should be of boolean type.'});
throw new Error('[generateDecoratorNode] "wordCount" should be of boolean type.');
}
});
/* c8 ignore stop */
}

/**
* @typedef {Object} DecoratorNodeProperty
* @property {string} name - The property's name.
* @property {*} default - The property's default value
* @property {('url'|'html'|'markdown'|null)} urlType - If the property contains a URL, the URL's type: 'url', 'html' or 'markdown'. Use 'url' is the property contains only a URL, 'html' or 'markdown' if the property contains HTML or markdown code, that may contain URLs.
* @property {boolean} wordCount - Whether the property should be counted in the word count
*
* @param {string} nodeType – The node's type (must be unique)
* @param {DecoratorNodeProperty[]} properties - An array of properties for the generated class
* @returns {Object} - The generated class.
*/
export function generateDecoratorNode({nodeType, properties = [], version = 1}) {
type PrivateKoenigProperty = KoenigDecoratorProperty & {privateName: string};

// NOTE: This is really what the return type is, but we wrap it in the GeneratedKoenigDecoratorNode class to make it a bit easier to interpret
// the 'magic' behind the scenes that generates the KoenigDecoratorNode classes.
// type GenerateKoenigDecoratorNodeFn = (options: GenerateKoenigDecoratorNodeOptions) => typeof generateDecoratorNode.prototype;
type GenerateKoenigDecoratorNodeFn = (options: GenerateKoenigDecoratorNodeOptions) => typeof GeneratedKoenigDecoratorNode;

type GenerateKoenigDecoratorNodeOptions = {
nodeType: string;
properties?: KoenigDecoratorNodeProperties;
version?: number;
};

type SerializedKoenigDecoratorNode = {
type: string;
version: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
};
class GeneratedKoenigDecoratorNode extends KoenigDecoratorNode {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(data?: any) {
super();
this.generateDecoratorNode(data);
}
}
export const generateDecoratorNode: GenerateKoenigDecoratorNodeFn = ({nodeType, properties = [], version = 1}) => {
validateArguments(nodeType, properties);

// Adds a `privateName` field to the properties for convenience (e.g. `__name`):
// properties: [{name: 'name', privateName: '__name', type: 'string', default: 'hello'}, {...}]
properties = properties.map((prop) => {
const __properties: PrivateKoenigProperty[] = properties.map((prop) => {
return {...prop, privateName: `__${prop.name}`};
});

class GeneratedDecoratorNode extends KoenigDecoratorNode {
constructor(data = {}, key) {
// allow any type here for ease of use
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(data: any = {}, key?: NodeKey) {
super(key);
properties.forEach((prop) => {
__properties.forEach((prop) => {
if (typeof prop.default === 'boolean') {
this[prop.privateName] = data[prop.name] ?? prop.default;
} else {
Expand All @@ -58,22 +95,11 @@ export function generateDecoratorNode({nodeType, properties = [], version = 1})
});
}

/**
* Returns the node's unique type
* @extends DecoratorNode
* @see https://lexical.dev/docs/concepts/nodes#extending-decoratornode
* @returns {string}
*/
static getType() {
static getType(): string {
return nodeType;
}

/**
* Creates a copy of an existing node with all its properties
* @extends DecoratorNode
* @see https://lexical.dev/docs/concepts/nodes#extending-decoratornode
*/
static clone(node) {
static clone(node: KoenigDecoratorNode) {
return new this(node.getDataset(), node.__key);
}

Expand All @@ -83,9 +109,10 @@ export function generateDecoratorNode({nodeType, properties = [], version = 1})
* @see https://github.com/TryGhost/SDK/tree/main/packages/url-utils
*/
static get urlTransformMap() {
let map = {};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const map: any = {};

properties.forEach((prop) => {
__properties.forEach((prop) => {
if (prop.urlType) {
map[prop.name] = prop.urlType;
}
Expand All @@ -100,9 +127,9 @@ export function generateDecoratorNode({nodeType, properties = [], version = 1})
*/
getDataset() {
const self = this.getLatest();

let dataset = {};
properties.forEach((prop) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const dataset: any = {};
__properties.forEach((prop) => {
dataset[prop.name] = self[prop.privateName];
});

Expand All @@ -115,10 +142,12 @@ export function generateDecoratorNode({nodeType, properties = [], version = 1})
* @extends DecoratorNode
* @param {Object} serializedNode - Lexical's representation of the node, in JSON format
*/
static importJSON(serializedNode) {
const data = {};

properties.forEach((prop) => {
static importJSON(serializedNode: SerializedKoenigDecoratorNode): KoenigDecoratorNode {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data: any = {};

__properties.forEach((prop) => {
data[prop.name] = serializedNode[prop.name];
});

Expand All @@ -130,11 +159,12 @@ export function generateDecoratorNode({nodeType, properties = [], version = 1})
* @extends DecoratorNode
* @see https://lexical.dev/docs/concepts/serialization#lexicalnodeexportjson
*/
exportJSON() {
exportJSON(): SerializedKoenigDecoratorNode {
const dataset = {
type: nodeType,
version: version,
...properties.reduce((obj, prop) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...__properties.reduce((obj: any, prop) => {
obj[prop.name] = this[prop.name];
return obj;
}, {})
Expand All @@ -143,56 +173,37 @@ export function generateDecoratorNode({nodeType, properties = [], version = 1})
}

/* c8 ignore start */
/**
* Inserts node in the DOM. Required when extending the DecoratorNode.
* @extends DecoratorNode
* @see https://lexical.dev/docs/concepts/nodes#extending-decoratornode
*/
createDOM() {
createDOM(): HTMLElement {
return document.createElement('div');
}

/**
* Required when extending the DecoratorNode
* @extends DecoratorNode
* @see https://lexical.dev/docs/concepts/nodes#extending-decoratornode
*/
updateDOM() {
updateDOM(): false {
return false;
}

/**
* Defines whether a node is a top-level block.
* @see https://lexical.dev/docs/api/classes/lexical.DecoratorNode#isinline
*/
isInline() {
// Defines whether a node is a top-level block.
isInline(): false {
// All our cards are top-level blocks. Override if needed.
return false;
}
/* c8 ignore stop */

/**
* Defines whether a node has dynamic data that needs to be fetched from the server when rendering
*/
hasDynamicData() {
// Defines whether a node has dynamic data that needs to be fetched from the server when rendering
hasDynamicData(): false {
return false;
}

/**
* Defines whether a node has an edit mode in the editor UI
*/
hasEditMode() {
// Defines whether a node has an edit mode in the editor UI
hasEditMode(): true {
// Most of our cards have an edit mode. Override if needed.
return true;
}

/*
* Returns the text content of the node, used by the editor to calculate the word count
* This method filters out properties without `wordCount: true`
*/
getTextContent() {
// Returns the text content of the node, used by the editor to calculate the word count
// This method filters out properties without `wordCount: true`
getTextContent(): string {
const self = this.getLatest();
const propertiesWithText = properties.filter(prop => !!prop.wordCount);
const propertiesWithText = __properties.filter(prop => !!prop.wordCount);

const text = propertiesWithText.map(
prop => readTextContent(self, prop.name)
Expand Down Expand Up @@ -220,7 +231,7 @@ export function generateDecoratorNode({nodeType, properties = [], version = 1})
*
* They can be used as `node.content` (getter) and `node.content = 'new value'` (setter)
*/
properties.forEach((prop) => {
__properties.forEach((prop) => {
Object.defineProperty(GeneratedDecoratorNode.prototype, prop.name, {
get: function () {
const self = this.getLatest();
Expand All @@ -234,4 +245,4 @@ export function generateDecoratorNode({nodeType, properties = [], version = 1})
});

return GeneratedDecoratorNode;
}
};
Loading