diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index a19a26b..501ce1f 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: - node-version: [10.x, 12.x, 14.x, 15.x] + node-version: [16.x, 18.x, 20.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: diff --git a/packages/to-dom-nodes/src/index.ts b/packages/to-dom-nodes/src/index.ts index 50322be..8a75012 100644 --- a/packages/to-dom-nodes/src/index.ts +++ b/packages/to-dom-nodes/src/index.ts @@ -20,6 +20,7 @@ import { RenderResult, RenderRule, StructuredText as StructuredTextGraphQlResponse, + TypesafeStructured as TypesafeStructuredTextGraphQlResponse, } from 'datocms-structured-text-utils'; import hyperscript from 'hyperscript'; @@ -30,6 +31,7 @@ export { renderNodeRule as renderRule }; export type { StructuredTextDocument, + TypesafeStructuredTextGraphQlResponse, StructuredTextGraphQlResponse, StructuredTextGraphQlResponseRecord, }; diff --git a/packages/to-html-string/src/index.ts b/packages/to-html-string/src/index.ts index 4341676..919f662 100644 --- a/packages/to-html-string/src/index.ts +++ b/packages/to-html-string/src/index.ts @@ -20,6 +20,7 @@ import { RenderResult, RenderRule, StructuredText as StructuredTextGraphQlResponse, + TypesafeStructuredText as TypesafeStructuredTextGraphQlResponse, } from 'datocms-structured-text-utils'; import vhtml from 'vhtml'; @@ -30,6 +31,7 @@ export { renderNodeRule as renderRule }; export type { StructuredTextDocument, + TypesafeStructuredTextGraphQlResponse, StructuredTextGraphQlResponse, StructuredTextGraphQlResponseRecord, }; diff --git a/packages/to-plain-text/src/index.ts b/packages/to-plain-text/src/index.ts index 8fcb7f0..6949b51 100644 --- a/packages/to-plain-text/src/index.ts +++ b/packages/to-plain-text/src/index.ts @@ -20,6 +20,7 @@ import { RenderResult, RenderRule, StructuredText as StructuredTextGraphQlResponse, + TypesafeStructuredText as TypesafeStructuredTextGraphQlResponse, } from 'datocms-structured-text-utils'; export { renderNodeRule, renderMarkRule, RenderError }; @@ -27,6 +28,7 @@ export { renderNodeRule, renderMarkRule, RenderError }; export { renderNodeRule as renderRule }; export type { StructuredTextDocument, + TypesafeStructuredTextGraphQlResponse, StructuredTextGraphQlResponse, StructuredTextGraphQlResponseRecord, }; diff --git a/packages/utils/src/guards.ts b/packages/utils/src/guards.ts index 0b1b0e6..b624357 100644 --- a/packages/utils/src/guards.ts +++ b/packages/utils/src/guards.ts @@ -15,13 +15,14 @@ import { WithChildrenNode, InlineNode, NodeType, - Record, + Record as DatoCmsRecord, StructuredText, ThematicBreak, Document, } from './types'; import { + allowedNodeTypes, headingNodeType, spanNodeType, rootNodeType, @@ -98,33 +99,50 @@ export function isThematicBreak(node: Node): node is ThematicBreak { return node.type === thematicBreakNodeType; } -export function isStructuredText( - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types - obj: any, -): obj is StructuredText { - return obj && 'value' in obj && isDocument(obj.value); +function isObject(obj: unknown): obj is Record { + return Boolean(typeof obj === 'object' && obj); } -export function isDocument( - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types - obj: any, -): obj is Document { - return obj && 'schema' in obj && 'document' in obj; +export function isNodeType(value: string): value is NodeType { + return allowedNodeTypes.includes(value as NodeType); } -export function isEmptyDocument( - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types - obj: any, -): boolean { +export function isNode(obj: unknown): obj is Node { + return Boolean( + isObject(obj) && + 'type' in obj && + typeof obj.type === 'string' && + isNodeType(obj.type), + ); +} + +export function isStructuredText< + R1 extends DatoCmsRecord, + R2 extends DatoCmsRecord = R1 +>(obj: unknown): obj is StructuredText { + return Boolean(isObject(obj) && 'value' in obj && isDocument(obj.value)); +} + +export function isDocument(obj: unknown): obj is Document { + return Boolean( + isObject(obj) && + 'schema' in obj && + 'document' in obj && + obj.schema === 'dast', + ); +} + +export function isEmptyDocument(obj: unknown): boolean { if (!obj) { return true; } - const document = isStructuredText(obj) - ? obj.value - : isDocument(obj) - ? obj - : null; + const document = + isStructuredText(obj) && isDocument(obj.value) + ? obj.value + : isDocument(obj) + ? obj + : null; if (!document) { throw new Error( @@ -133,7 +151,6 @@ export function isEmptyDocument( } return ( - document.schema === 'dast' && document.document.children.length === 1 && document.document.children[0].type === 'paragraph' && document.document.children[0].children.length === 1 && diff --git a/packages/utils/src/render.ts b/packages/utils/src/render.ts index be73fa9..2cc32fb 100644 --- a/packages/utils/src/render.ts +++ b/packages/utils/src/render.ts @@ -1,5 +1,5 @@ import { Node, Record, Document, StructuredText } from './types'; -import { hasChildren, isDocument, isStructuredText } from './guards'; +import { hasChildren, isDocument, isNode, isStructuredText } from './guards'; import { flatten } from 'array-flatten'; export class RenderError extends Error { @@ -90,12 +90,11 @@ export function transformNode< if (matchingTransform) { return matchingTransform.apply({ adapter, node, children, key, ancestors }); - } else { - throw new RenderError( - `Don't know how to render a node with type "${node.type}". Please specify a custom renderRule for it!`, - node, - ); } + throw new RenderError( + `Don't know how to render a node with type "${node.type}". Please specify a custom renderRule for it!`, + node, + ); } export type Adapter< @@ -128,18 +127,23 @@ export function render< return null; } - const result = transformNode( - adapter, - isStructuredText(structuredTextOrNode) + const node = + isStructuredText(structuredTextOrNode) && + isDocument(structuredTextOrNode.value) ? structuredTextOrNode.value.document : isDocument(structuredTextOrNode) ? structuredTextOrNode.document - : structuredTextOrNode, + : isNode(structuredTextOrNode) + ? structuredTextOrNode + : undefined; - 't-0', - [], - renderRules, - ); + if (!node) { + throw new Error( + 'Passed object is neither null, a Structured Text value, a DAST document or a DAST node', + ); + } + + const result = transformNode(adapter, node, 't-0', [], renderRules); return result; } diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index aa85f3d..291968e 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -440,8 +440,31 @@ export type NodeType = * and embedded within the flow of the text. */ -export type StructuredText = { - /** A DatoCMS compatible document */ +export type StructuredText< + R1 extends Record = Record, + R2 extends Record = R1 +> = { + /** + * A DatoCMS "dast" document + * + * https://www.datocms.com/docs/structured-text/dast + */ + value: Document | unknown; + /** Blocks associated with the Structured Text */ + blocks?: R1[]; + /** Links associated with the Structured Text */ + links?: R2[]; +}; + +export type TypesafeStructuredText< + R1 extends Record = Record, + R2 extends Record = R1 +> = { + /** + * A DatoCMS "dast" document + * + * https://www.datocms.com/docs/structured-text/dast + */ value: Document; /** Blocks associated with the Structured Text */ blocks?: R1[];