Conversation
WalkthroughThe PR migrates the kg-default-nodes package from a JS/lib+Rollup layout to a TypeScript-first src/build layout. It removes numerous legacy lib files and Rollup config, adds a complete typed src/ tree (nodes, parsers, renderers, serializers, utils), updates package.json and tsconfig files for tsc/tsx builds, replaces the ESLint config with a TypeScript-targeted defineConfig form, and migrates tests and test-utils to ESM/TypeScript (including new type declarations and test bootstrap changes). Several public entrypoint re-exports were moved/removed and many runtime APIs gained explicit TypeScript signatures. Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 11
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (8)
packages/kg-default-nodes/src/utils/size-byte-converter.ts (1)
1-25:⚠️ Potential issue | 🟡 MinorHandle malformed and out-of-range sizes before formatting.
sizeToBytes('foo KB')currently returnsNaN, andbytesToSize(1024 ** 5)returns1 undefinedbecause the unit index runs pastTB. The new type annotations do not protect either runtime value, so imported file metadata can still leak broken text into rendering.Possible fix
export function sizeToBytes(size: string) { if (!size) { return 0; } const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; - const sizeParts = size.split(' '); - const sizeNumber = parseFloat(sizeParts[0]); - const sizeUnit = sizeParts[1]; + const [rawNumber = '', rawUnit = ''] = size.trim().split(/\s+/, 2); + const sizeNumber = Number(rawNumber); + const sizeUnit = rawUnit === 'Byte' ? 'Bytes' : rawUnit; const sizeUnitIndex = sizes.indexOf(sizeUnit); - if (sizeUnitIndex === -1) { + if (!Number.isFinite(sizeNumber) || sizeNumber < 0 || sizeUnitIndex === -1) { return 0; } return Math.round(sizeNumber * Math.pow(1024, sizeUnitIndex)); } export function bytesToSize(bytes: number) { - if (!bytes) { + if (!Number.isFinite(bytes) || bytes < 1) { return '0 Byte'; } const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; - if (bytes === 0) { - return '0 Byte'; - } - const i = Math.floor(Math.log(bytes) / Math.log(1024)); + const i = Math.min(sizes.length - 1, Math.floor(Math.log(bytes) / Math.log(1024))); return Math.round((bytes / Math.pow(1024, i))) + ' ' + sizes[i]; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/utils/size-byte-converter.ts` around lines 1 - 25, sizeToBytes: validate the input string and parsed number before computing—ensure size is a non-empty string, split yields at least 2 parts, parseFloat returns a finite number, and the unit is one of the allowed values (sizes array); if any check fails return 0. bytesToSize: validate bytes is a finite non-negative number and handle the unit index overflow by clamping the computed index i to the last index of sizes (so very large values map to 'TB' instead of undefined); also treat 0 or negative/invalid bytes as '0 Byte'. Use the existing function names sizeToBytes and bytesToSize and the sizes array to locate where to add these guards and clamping logic.packages/kg-default-nodes/src/nodes/header/parsers/header-parser.ts (1)
18-40:⚠️ Potential issue | 🟡 MinorCoerce nullable DOM reads back to strings before
new HeaderNode(payload).
getAttribute()andtextContentcan still returnnull, butpackages/kg-default-nodes/src/nodes/header/HeaderNode.tsdeclares these fields as strings. Packing them intoRecord<string, unknown>only hides the mismatch and lets malformed imports storenullinto string properties. Use|| ''(or omit the key) for the nullable reads before constructing the node.Also applies to: 57-84
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/header/parsers/header-parser.ts` around lines 18 - 40, The DOM reads for backgroundImageSrc, header, subheader, buttonUrl and buttonText can be null but are passed into new HeaderNode(payload) where those fields are declared as strings; coerce each nullable value to a string (e.g. using value || '') before building the payload in header-parser.ts so HeaderNode never receives nulls (apply same fix for the other block referenced around lines 57-84); ensure you update variables backgroundImageSrc, header, subheader, buttonUrl and buttonText (or omit keys) prior to constructing the HeaderNode instance.packages/kg-default-nodes/src/nodes/button/button-renderer.ts (1)
50-72:⚠️ Potential issue | 🟠 MajorEscape
buttonTextandbuttonUrlbefore interpolating email HTML.
packages/kg-default-nodes/src/utils/tagged-template-fns.tsis raw string concatenation, so these branches inject node data straight intoinnerHTML. A button label containing</&or a crafted URL with quotes will break the markup and creates an HTML-injection sink. Escape the dynamic values first, and ideally whitelistalignmentrather than interpolating it raw.Possible fix
+import {escapeHtml} from '../../utils/escape-html.js'; + function emailTemplate(node: ButtonNodeData, options: RenderOptions, document: Document) { const {buttonUrl, buttonText} = node; + const safeButtonUrl = escapeHtml(buttonUrl); + const safeButtonText = escapeHtml(buttonText); @@ - <a href="${buttonUrl}">${buttonText}</a> + <a href="${safeButtonUrl}">${safeButtonText}</a> @@ - <a href="${buttonUrl}">${buttonText}</a> + <a href="${safeButtonUrl}">${safeButtonText}</a>Also applies to: 97-111
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/button/button-renderer.ts` around lines 50 - 72, In emailTemplate, avoid injecting raw node.buttonText and node.buttonUrl into cardHtml and element.innerHTML: HTML-escape buttonText and properly validate/encode buttonUrl before interpolation, and whitelist node.alignment to a small set of allowed values (e.g., "left","center","right") instead of interpolating it raw; then build the string using the escaped values (or set element.textContent / anchor.href on created elements instead of innerHTML) and replace the existing innerHTML assignments in emailTemplate and the other identical branch that also sets innerHTML so dynamic values cannot break the markup or create an HTML-injection sink.packages/kg-default-nodes/test/nodes/markdown.test.ts (1)
40-43:⚠️ Potential issue | 🟡 MinorMisleading test description: refers to
$isImageNodeinstead of$isMarkdownNode.The test description says "matches node with $isImageNode" but the test actually verifies
$isMarkdownNode. This appears to be a copy-paste error.📝 Suggested fix
- it('matches node with $isImageNode', editorTest(function () { + it('matches node with $isMarkdownNode', editorTest(function () { const markdownNode = $createMarkdownNode(dataset); $isMarkdownNode(markdownNode).should.be.true(); }));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/markdown.test.ts` around lines 40 - 43, The test description is incorrect: update the it(...) description string that currently reads 'matches node with $isImageNode' to correctly refer to $isMarkdownNode (or a neutral description like 'matches node with $isMarkdownNode') so it matches the assertion using $createMarkdownNode and $isMarkdownNode in the test; ensure the test name reflects the behavior being verified in the it block.packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts (1)
27-31:⚠️ Potential issue | 🟡 MinorType mismatch:
imageDatatypes don't align withCallToActionNodeinterface.The inline type for
imageDataspecifiesimageUrl: string | numberand dimensions asstring | number | null, but theCallToActionNodeinterface (from the relevant snippet) definesimageUrl: string | nulland dimensions asnumber | null. This inconsistency could cause type errors downstream.🔧 Proposed fix to align types
- const imageData: {imageUrl: string | number; imageWidth: string | number | null; imageHeight: string | number | null} = { + const imageData: {imageUrl: string; imageWidth: number | null; imageHeight: number | null} = { imageUrl: '', imageWidth: null, imageHeight: null };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts` around lines 27 - 31, The inline type and initial values for the imageData constant in calltoaction-parser.ts don't match the CallToActionNode interface; change imageData's type to imageUrl: string | null and imageWidth/imageHeight: number | null and update the initializer values accordingly (use null for missing values rather than '' or null-for-numeric mismatches) so imageData conforms to CallToActionNode.packages/kg-default-nodes/test/nodes/signup.test.ts (1)
688-694:⚠️ Potential issue | 🟡 MinorPotential test bug: Test creates a
PaywallNodeinstead ofSignupNode.The
getTextContenttest forSignupNodecreates aPaywallNodeinstead of aSignupNode. This appears to be unintentional and tests the wrong node type.🐛 Proposed fix
describe('getTextContent', function () { it('returns contents', editorTest(function () { - const node = $createPaywallNode({}); + const node = $createSignupNode({}); // signup nodes don't have text content node.getTextContent().should.equal(''); })); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/signup.test.ts` around lines 688 - 694, The test in the describe('getTextContent') block incorrectly instantiates a PaywallNode via $createPaywallNode; replace that with a SignupNode creation (use $createSignupNode or the factory used elsewhere for SignupNode) so the test exercises SignupNode.getTextContent(), keep the assertion that getTextContent() returns an empty string and leave the surrounding editorTest wrapper intact.packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts (1)
47-55:⚠️ Potential issue | 🟡 MinorPotential runtime error:
thumbnail_widthandthumbnail_heightmay be undefined.The
isVideoWithThumbnailcheck on line 47 only validatesthumbnail_url, but line 53 uses non-null assertions onthumbnail_widthandthumbnail_height. If these are undefined, the division will produceNaN, causing invalid spacer dimensions.🛡️ Proposed fix to guard thumbnail dimensions
- const isVideoWithThumbnail = node.embedType === 'video' && metadata && metadata.thumbnail_url; + const isVideoWithThumbnail = node.embedType === 'video' && metadata && metadata.thumbnail_url && metadata.thumbnail_width && metadata.thumbnail_height;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts` around lines 47 - 55, The code computes spacer dimensions using metadata.thumbnail_width and metadata.thumbnail_height with non-null assertions even though isVideoWithThumbnail only checked metadata.thumbnail_url; update the logic in embed-renderer.ts (the isVideoWithThumbnail / email thumbnail branch where spacerWidth/spacerHeight are computed) to first verify that metadata.thumbnail_width and metadata.thumbnail_height are present and are finite numbers before using them, and if not present fall back to safe defaults or skip creating the thumbnail spacer (e.g., use a default aspect ratio or skip the spacer calculation) so the division (thumbnail_width/thumbnail_height) cannot produce NaN or throw at runtime.packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts (1)
52-60:⚠️ Potential issue | 🔴 CriticalFix unanchored color regex allowing attribute injection.
The regex
/^[a-zA-Z\d-]+|#([a-fA-F\d]{3}|[a-fA-F\d]{6})$/lacks proper grouping around the alternation. The first alternative^[a-zA-Z\d-]+has no end anchor, so it matches any string starting with alphanumeric characters. This allows payloads likeaccent" onclick="..."to pass validation and be directly interpolated into HTML attributes (e.g.,class="kg-cta-bg-${dataset.backgroundColor}"), creating an attribute injection vulnerability.Apply the fix to both locations (lines 52-60 and 395-399):
Suggested fix
+const VALID_COLOR_PATTERN = /^(?:[a-zA-Z\d-]+|#(?:[a-fA-F\d]{3}|[a-fA-F\d]{6}))$/; + function ctaCardTemplate(dataset: CTADataset) { // Add validation for buttonColor - if (!dataset.buttonColor || !dataset.buttonColor.match(/^[a-zA-Z\d-]+|#([a-fA-F\d]{3}|[a-fA-F\d]{6})$/)) { + if (!dataset.buttonColor || !VALID_COLOR_PATTERN.test(dataset.buttonColor)) { dataset.buttonColor = 'accent'; }Also apply to the
backgroundColorvalidation (lines 395-399):- if (!dataset.backgroundColor || !dataset.backgroundColor.match(/^[a-zA-Z\d-]+|#([a-fA-F\d]{3}|[a-fA-F\d]{6})$/)) { + if (!dataset.backgroundColor || !VALID_COLOR_PATTERN.test(dataset.backgroundColor)) { dataset.backgroundColor = 'white'; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts` around lines 52 - 60, The regex used to validate dataset.buttonColor is unanchored around the alternation, allowing values like 'accent" onclick="...' to pass and enabling attribute injection; update the validation in ctaCardTemplate to use a single anchored grouped alternation (e.g., require the whole string to match either a simple token or a hex color) and keep the existing fallback to 'accent' when invalid, then mirror the same correction for dataset.backgroundColor validation elsewhere; specifically fix the validation logic that assigns dataset.buttonColor and dataset.backgroundColor so they use a grouped/anchored pattern (ensuring the full string is matched) before using the value in class or style interpolation.
🟡 Minor comments (14)
packages/kg-default-nodes/src/utils/truncate.ts-3-5 (1)
3-5:⚠️ Potential issue | 🟡 MinorGuard non-positive length inputs to avoid malformed truncation output.
At Line 5 and in
truncateHtml, non-positivemaxLength/maxLengthMobilecan lead to unintended substring behavior and output that violates expected max-length semantics.💡 Proposed fix
export function truncateText(text: string, maxLength: number) { + if (maxLength <= 0) { + return ''; + } if (text && text.length > maxLength) { return text.substring(0, maxLength - 1).trim() + '\u2026'; } else { return text ?? ''; } } export function truncateHtml(text: string, maxLength: number, maxLengthMobile?: number) { + if (maxLength <= 0) { + return ''; + } + if (typeof maxLengthMobile === 'number' && maxLengthMobile <= 0) { + return escapeHtml(truncateText(text, maxLength)); + } // If no mobile length specified or mobile length is larger than desktop, // just do a simple truncate if (!maxLengthMobile || maxLength <= maxLengthMobile) { return escapeHtml(truncateText(text, maxLength)); }Also applies to: 11-15
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/utils/truncate.ts` around lines 3 - 5, Guard against non-positive length inputs in truncateText and truncateHtml by returning an empty string when maxLength (or maxLengthMobile) is <= 0 to avoid calling substring with a negative length or producing malformed output; add an early check in truncateText (parameter maxLength) and in truncateHtml (parameter maxLengthMobile and any other maxLength usages) that returns '' for non-positive values, otherwise keep the existing substring/ellipsis logic (or use Math.max(0, maxLength - 1) if you prefer a defensive clamp before substring).packages/kg-default-nodes/src/utils/rgb-to-hex.ts-8-12 (1)
8-12:⚠️ Potential issue | 🟡 MinorGood null check, but incomplete validation for malformed RGB strings.
The null check handles the case where no digits are found, but a malformed string like
"rgb(255)"would pass this check and result ingandbbeingundefined, producing invalid hex output like"#ffNaNNaN".🛡️ Proposed fix to validate component count
const match = rgb.match(/\d+/g); - if (!match) { + if (!match || match.length < 3) { return null; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/utils/rgb-to-hex.ts` around lines 8 - 12, The current null check after const match = rgb.match(/\d+/g) only ensures digits exist but doesn't validate the count, so malformed inputs like "rgb(255)" yield undefined g/b; update the rgb-to-hex validation to verify match.length >= 3 before destructuring [r, g, b] (and return null on failure), and ideally coerce/validate each component is a number within 0–255 (in the function handling conversion, e.g., the rgbToHex helper that uses match and [r,g,b]) so you never produce NaN or invalid hex strings.packages/kg-default-nodes/src/utils/visibility.ts-43-49 (1)
43-49:⚠️ Potential issue | 🟡 MinorPotential runtime error from non-null assertions when
weborThe
isOldVisibilityFormatfunction uses non-null assertions (visibility.web!,visibility.email!) on lines 46-48, but these are only safe ifvisibility.webandvisibility.emailexist. The function checks for their presence usinghasOwnPropertyon lines 44-45, but if either check fails, the function should short-circuit and returntruebefore reaching the assertions.However, due to the
||chain, ifvisibility.webexists butvisibility.emaildoesn't, line 48 (visibility.email!.memberSegment) will throw.🐛 Proposed fix using optional chaining
export function isOldVisibilityFormat(visibility: Visibility) { return !Object.prototype.hasOwnProperty.call(visibility, 'web') || !Object.prototype.hasOwnProperty.call(visibility, 'email') - || !Object.prototype.hasOwnProperty.call(visibility.web!, 'nonMember') - || isNullish(visibility.web!.memberSegment) - || isNullish(visibility.email!.memberSegment); + || !Object.prototype.hasOwnProperty.call(visibility.web, 'nonMember') + || isNullish(visibility.web?.memberSegment) + || isNullish(visibility.email?.memberSegment); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/utils/visibility.ts` around lines 43 - 49, The function isOldVisibilityFormat uses non-null assertions (visibility.web! and visibility.email!) that can still throw when one of those props is missing; change the checks to safely access nested props with optional chaining and/or reorder to short-circuit: keep the hasOwnProperty checks for 'web' and 'email' and replace uses of visibility.web! and visibility.email! with visibility.web? and visibility.email? (e.g., check hasOwnProperty.call(visibility.web, 'nonMember') replaced or guarded and use isNullish(visibility.web?.memberSegment) and isNullish(visibility.email?.memberSegment)) so the function returns true when web/email are absent without dereferencing undefined in isOldVisibilityFormat.packages/kg-default-nodes/src/nodes/image/ImageNode.ts-4-13 (1)
4-13:⚠️ Potential issue | 🟡 MinorType mismatch:
width/heightare nullable here but non-nullable inImageNodeData.The
ImageNodeinterface defineswidthandheightasnumber | null, butImageNodeDatainimage-renderer.ts(lines 10-11) defines them asnumber. This inconsistency can lead to type errors when passingImageNodedata to the renderer, since the renderer assumes non-null values.Consider aligning these types—either make
ImageNodeData.width/heightnullable, or ensure the renderer handles null values.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/image/ImageNode.ts` around lines 4 - 13, The types for image dimensions are inconsistent: ImageNode declares width and height as number | null while ImageNodeData (in image-renderer.ts) declares them as number; align them by making ImageNodeData.width and ImageNodeData.height nullable (number | null) or update the renderer to handle nulls before using ImageNodeData (e.g., provide fallback values or conditional logic). Locate the ImageNode interface and the ImageNodeData type in image-renderer.ts and choose one approach—either change ImageNodeData to number | null to match ImageNode, or add null-checks/defaults in the renderer where ImageNodeData.width/height are accessed.packages/kg-default-nodes/src/nodes/image/image-renderer.ts-132-133 (1)
132-133:⚠️ Potential issue | 🟡 MinorNon-null assertion on regex match risks runtime error.
If
node.srcdoesn't match the expected pattern/(.*\/content\/images)\/(.*)/, the!assertion will cause a runtime error when destructuring. WhileisLocalContentImageat line 124 provides some validation, it uses a different regex pattern that may not guarantee this match succeeds.🛡️ Proposed defensive check
- const [, imagesPath, filename] = node.src.match(/(.*\/content\/images)\/(.*)/)!; - img.setAttribute('src', `${imagesPath}/size/w${srcWidth}/${filename}`); + const match = node.src.match(/(.*\/content\/images)\/(.*)/); + if (match) { + const [, imagesPath, filename] = match; + img.setAttribute('src', `${imagesPath}/size/w${srcWidth}/${filename}`); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/image/image-renderer.ts` around lines 132 - 133, The destructuring uses a non-null assertion on node.src.match(...) which can crash if the regex doesn't match; replace it by storing the match result in a variable (e.g., const m = node.src.match(/(.*\/content\/images)\/(.*)/)) and add a defensive check (if (!m) { handle gracefully — e.g., skip size rewrite, log a warning, or leave img.src as node.src }) before using m[1]/m[2]; update the code around the image-rendering logic (the lines that set img.setAttribute('src', ...)) to use the validated match parts so no runtime error occurs when the pattern isn't present.packages/kg-default-nodes/test/serializers/paragraph.test.ts-8-8 (1)
8-8:⚠️ Potential issue | 🟡 MinorDescribe block name doesn't match filename.
The describe block says
'Serializers: linebreak'but the file is namedparagraph.test.ts. This mismatch could cause confusion. Consider renaming either the describe block to match the filename or vice versa.Suggested fix
-describe('Serializers: linebreak', function () { +describe('Serializers: paragraph', function () {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/serializers/paragraph.test.ts` at line 8, The describe block string 'Serializers: linebreak' does not match the test filename paragraph.test.ts; rename the describe block to 'Serializers: paragraph' (or rename the file to match if intended) so the describe label and filename are consistent; update the string in the describe(...) call to match paragraph.test.ts and run tests to confirm no other references need changing.packages/kg-default-nodes/src/utils/tagged-template-fns.ts-8-10 (1)
8-10:⚠️ Potential issue | 🟡 MinorPreserve falsy interpolation values.
Current interpolation on Line 9 converts
0/falseto empty string. Use nullish coalescing instead.🐛 Proposed fix
- const result = strings.reduce((acc: string, str: string, i: number) => { - return acc + str + (values[i] || ''); + const result = strings.reduce((acc: string, str: string, i: number) => { + return acc + str + String(values[i] ?? ''); }, '');🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/utils/tagged-template-fns.ts` around lines 8 - 10, The interpolation reducer in the tagged-template function uses (values[i] || '') which drops falsy but valid values like 0 or false; update the reduction logic in the strings.reduce callback (the expression that computes result) to use nullish coalescing (values[i] ?? '') so null/undefined become empty strings while preserving 0 and false.packages/kg-default-nodes/src/nodes/embed/types/twitter.ts-79-83 (1)
79-83:⚠️ Potential issue | 🟡 MinorNon-null assertions on optional fields may cause runtime errors.
public_metricsandcreated_atare typed as optional inTweetData, but the code uses!to assert they exist. If Twitter API returns incomplete data, this will throw at runtime.Consider adding defensive checks or providing fallback values:
🛡️ Suggested defensive handling
- const retweetCount = numberFormatter.format(tweetData.public_metrics!.retweet_count); - const likeCount = numberFormatter.format(tweetData.public_metrics!.like_count); + const retweetCount = numberFormatter.format(tweetData.public_metrics?.retweet_count ?? 0); + const likeCount = numberFormatter.format(tweetData.public_metrics?.like_count ?? 0); const authorUser = tweetData.users && tweetData.users.find((user: TwitterUser) => user.id === tweetData.author_id); - const tweetTime = DateTime.fromISO(tweetData.created_at!).toLocaleString(DateTime.TIME_SIMPLE); - const tweetDate = DateTime.fromISO(tweetData.created_at!).toLocaleString(DateTime.DATE_MED); + const tweetTime = tweetData.created_at ? DateTime.fromISO(tweetData.created_at).toLocaleString(DateTime.TIME_SIMPLE) : ''; + const tweetDate = tweetData.created_at ? DateTime.fromISO(tweetData.created_at).toLocaleString(DateTime.DATE_MED) : '';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/embed/types/twitter.ts` around lines 79 - 83, The code uses non-null assertions on tweetData.public_metrics and tweetData.created_at when computing retweetCount, likeCount, tweetTime and tweetDate; replace these assertions with defensive checks: verify tweetData.public_metrics exists before formatting retweet_count/like_count and provide default values (e.g., 0 or "0") for retweetCount/likeCount, and verify tweetData.created_at is present before calling DateTime.fromISO, providing a fallback string (e.g., empty or "Unknown date") for tweetTime/tweetDate; also keep the authorUser lookup as-is but handle a potential undefined authorUser when later used. Ensure you update the logic around retweetCount, likeCount, tweetTime, and tweetDate to use optional chaining and defaults instead of the ! operator.packages/kg-default-nodes/src/nodes/embed/types/twitter.ts-92-94 (1)
92-94:⚠️ Potential issue | 🟡 MinorNon-null assertion on
includesmay fail if attachments exist but media is missing.The condition checks
tweetData.attachments?.media_keysbut then assumestweetData.includes!.media[0]exists. Ifincludesis undefined ormediais empty, this will throw.🛡️ Suggested defensive handling
let tweetImageUrl = null; const hasImageOrVideo = tweetData.attachments && tweetData.attachments && tweetData.attachments.media_keys; - if (hasImageOrVideo) { - tweetImageUrl = tweetData.includes!.media[0].preview_image_url || tweetData.includes!.media[0].url; + if (hasImageOrVideo && tweetData.includes?.media?.[0]) { + tweetImageUrl = tweetData.includes.media[0].preview_image_url || tweetData.includes.media[0].url; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/embed/types/twitter.ts` around lines 92 - 94, The code assigns tweetImageUrl using tweetData.includes!.media[0] while only checking tweetData.attachments; update the check and assignment around hasImageOrVideo and tweetImageUrl to defensively verify that tweetData.includes exists and includes.media is a non-empty array (and ideally that a media entry matching an attachments.media_keys exists) before accessing media[0]; change references in this block (hasImageOrVideo, tweetImageUrl, and the tweetData.includes usage) to use optional chaining and length checks or lookup by media_key so you never dereference includes or media when they are undefined/empty.packages/kg-default-nodes/test/nodes/file.test.ts-67-68 (1)
67-68:⚠️ Potential issue | 🟡 MinorType mismatch:
fileSizeis declared asstringbut should benumberfor consistency.The
FileNodeinterface declaresfileSize: string(line 11 of FileNode.ts), yet thefile-rendererinterface expectsfileSize: number(line 11 of file-renderer.ts). The test assignment at line 67 uses a double castas unknown as stringto bypass type checking and assign a number, then asserts it equals the number123456at line 68. Additionally, FileNode.ts line 46 converts fileSize to a number before use (Number(this.fileSize)). This type inconsistency should be resolved by either changing the FileNode interface to declarefileSize: numberor ensuring all usages treat it consistently as a string.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/file.test.ts` around lines 67 - 68, The test is forcing a number into FileNode.fileSize despite FileNode declaring fileSize as string; make fileSize consistently a number across the codebase by changing the FileNode interface/implementation to declare fileSize: number (update any constructors or parsing logic that currently converts via Number(this.fileSize)), update file-renderer expectations if necessary, and adjust the test in file.test.ts to assign 123456 without casts and assert equality; ensure all uses of FileNode.fileSize (e.g., in FileNode class methods and file-renderer) treat it as a number so no runtime Number(...) conversions are required.packages/kg-default-nodes/src/nodes/file/FileNode.ts-6-12 (1)
6-12:⚠️ Potential issue | 🟡 MinorType inconsistency:
fileSizedeclared asstringbut used asnumberthroughout.The interface declares
fileSize: string, but the actual usage suggests it should benumber:
formattedFileSizecallsNumber(this.fileSize)(line 46)- Tests assign numeric values
- The property represents byte count, which is naturally numeric
Consider updating the type to
numberornumber | stringto match actual usage.♻️ Suggested interface update
export interface FileNode { src: string; fileTitle: string; fileCaption: string; fileName: string; - fileSize: string; + fileSize: number; }Note: This would also require updating the property default from
''to0ornull.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/file/FileNode.ts` around lines 6 - 12, The FileNode interface incorrectly types fileSize as string while code (formattedFileSize) and tests treat it as a numeric byte count; change the FileNode declaration to use fileSize: number (or number | string if you need backward compatibility), update any default initialization from '' to 0 or null, and adjust related constructors/assignments and tests to pass numeric values; ensure formattedFileSize no longer needs Number(this.fileSize) if you make it a number and update any places that assumed a string.packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts-237-239 (1)
237-239:⚠️ Potential issue | 🟡 MinorRedundant
createDocument!()call.A
documentis already created at Line 215. Creating anotheremailDocis unnecessary overhead.🔧 Proposed fix
if (options.target === 'email') { - const emailDoc = options.createDocument!(); - const emailDiv = emailDoc.createElement('div'); + const emailDiv = document.createElement('div'); emailDiv.innerHTML = emailTemplate(node, options)?.trim();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts` around lines 237 - 239, The new call to options.createDocument!() is redundant because this renderer already creates a document earlier; replace usage of the newly introduced emailDoc with the existing document variable (the document created earlier in this renderer) when target === 'email', i.e. create emailDiv via the existing document.createElement instead of calling options.createDocument(), and remove the emailDoc variable and its createDocument invocation (adjust references to emailDiv accordingly); keep using options.target and options.createDocument where needed only for initial document creation.packages/kg-default-nodes/src/nodes/TKNode.ts-17-20 (1)
17-20:⚠️ Potential issue | 🟡 MinorFilter empty theme class tokens before
classList.add().
config.theme.tk?.split(' ')produces empty strings for cases like''or'a b', andclassList.add('')throws aSyntaxErrorper the DOM specification.packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.tsalready filters these tokens (lines 42-43), soTKNodeshould do the same.Suggested fix
- const classes = config.theme.tk?.split(' ') || []; - element.classList.add(...classes); + const classes = config.theme.tk?.split(' ').filter(Boolean) ?? []; + element.classList.add(...classes);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/TKNode.ts` around lines 17 - 20, The TKNode.createDOM method currently builds classes with config.theme.tk?.split(' ') then calls element.classList.add(...classes), which can pass empty strings and throw; update createDOM (TKNode.createDOM) to filter out empty tokens from the split (e.g., use .filter(Boolean) or equivalent) before calling element.classList.add so only non-empty class names are added to the element.packages/kg-default-nodes/src/nodes/audio/audio-parser.ts-26-30 (1)
26-30:⚠️ Potential issue | 🟡 MinorReject malformed durations instead of storing
NaN.The
catchblock here won't catchparseInt()failures with string inputs—it silently returnsNaNinstead. Malformed input like1:xxwill assignNaNtopayload.duration. Validate that both parsed values are numbers before assigning.Suggested fix
if (durationText) { const [minutes, seconds = '0'] = durationText.split(':'); - try { - payload.duration = parseInt(minutes) * 60 + parseInt(seconds); - } catch { - // ignore duration - } + const parsedMinutes = Number.parseInt(minutes, 10); + const parsedSeconds = Number.parseInt(seconds, 10); + + if (!Number.isNaN(parsedMinutes) && !Number.isNaN(parsedSeconds)) { + payload.duration = parsedMinutes * 60 + parsedSeconds; + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/audio/audio-parser.ts` around lines 26 - 30, The current parsing of durationText (in the audio parsing logic where durationText is split and parsed) can yield NaN because parseInt does not throw; replace the try/catch approach by explicitly validating the numeric results: split durationText into minutes and seconds, parse them, then check both parsed values with Number.isFinite (or !Number.isNaN) before assigning payload.duration; if validation fails, do not set payload.duration (or remove it) and handle the malformed input path instead of assigning NaN. Ensure you update the code around the durationText handling and payload.duration assignment (the block that destructures [minutes, seconds = '0'] and sets payload.duration).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f4d28d24-3b2a-4673-8324-4dde83b5928c
⛔ Files ignored due to path filters (1)
packages/kg-default-nodes/src/nodes/at-link/kg-link.svgis excluded by!**/*.svg
📒 Files selected for processing (161)
packages/kg-default-nodes/eslint.config.mjspackages/kg-default-nodes/index.jspackages/kg-default-nodes/lib/kg-default-nodes.jspackages/kg-default-nodes/lib/nodes/at-link/index.jspackages/kg-default-nodes/lib/nodes/codeblock/codeblock-parser.jspackages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-parser.jspackages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-renderer.jspackages/kg-default-nodes/lib/nodes/html/html-parser.jspackages/kg-default-nodes/lib/nodes/markdown/markdown-renderer.jspackages/kg-default-nodes/lib/nodes/paywall/paywall-parser.jspackages/kg-default-nodes/lib/utils/is-unsplash-image.jspackages/kg-default-nodes/package.jsonpackages/kg-default-nodes/rollup.config.mjspackages/kg-default-nodes/src/KoenigDecoratorNode.tspackages/kg-default-nodes/src/generate-decorator-node.tspackages/kg-default-nodes/src/index.tspackages/kg-default-nodes/src/kg-default-nodes.tspackages/kg-default-nodes/src/nodes/ExtendedHeadingNode.tspackages/kg-default-nodes/src/nodes/ExtendedQuoteNode.tspackages/kg-default-nodes/src/nodes/ExtendedTextNode.tspackages/kg-default-nodes/src/nodes/TKNode.tspackages/kg-default-nodes/src/nodes/aside/AsideNode.tspackages/kg-default-nodes/src/nodes/aside/AsideParser.tspackages/kg-default-nodes/src/nodes/at-link/AtLinkNode.tspackages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.tspackages/kg-default-nodes/src/nodes/at-link/index.tspackages/kg-default-nodes/src/nodes/audio/AudioNode.tspackages/kg-default-nodes/src/nodes/audio/audio-parser.tspackages/kg-default-nodes/src/nodes/audio/audio-renderer.tspackages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.tspackages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.tspackages/kg-default-nodes/src/nodes/bookmark/bookmark-renderer.tspackages/kg-default-nodes/src/nodes/button/ButtonNode.tspackages/kg-default-nodes/src/nodes/button/button-parser.tspackages/kg-default-nodes/src/nodes/button/button-renderer.tspackages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.tspackages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.tspackages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.tspackages/kg-default-nodes/src/nodes/callout/CalloutNode.tspackages/kg-default-nodes/src/nodes/callout/callout-parser.tspackages/kg-default-nodes/src/nodes/callout/callout-renderer.tspackages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.tspackages/kg-default-nodes/src/nodes/codeblock/codeblock-parser.tspackages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.tspackages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.tspackages/kg-default-nodes/src/nodes/email-cta/email-cta-renderer.tspackages/kg-default-nodes/src/nodes/email/EmailNode.tspackages/kg-default-nodes/src/nodes/email/email-renderer.tspackages/kg-default-nodes/src/nodes/embed/EmbedNode.tspackages/kg-default-nodes/src/nodes/embed/embed-parser.tspackages/kg-default-nodes/src/nodes/embed/embed-renderer.tspackages/kg-default-nodes/src/nodes/embed/types/twitter.tspackages/kg-default-nodes/src/nodes/file/FileNode.tspackages/kg-default-nodes/src/nodes/file/file-parser.tspackages/kg-default-nodes/src/nodes/file/file-renderer.tspackages/kg-default-nodes/src/nodes/gallery/GalleryNode.tspackages/kg-default-nodes/src/nodes/gallery/gallery-parser.tspackages/kg-default-nodes/src/nodes/gallery/gallery-renderer.tspackages/kg-default-nodes/src/nodes/header/HeaderNode.tspackages/kg-default-nodes/src/nodes/header/parsers/header-parser.tspackages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.tspackages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.tspackages/kg-default-nodes/src/nodes/horizontalrule/HorizontalRuleNode.tspackages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-parser.tspackages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-renderer.tspackages/kg-default-nodes/src/nodes/html/HtmlNode.tspackages/kg-default-nodes/src/nodes/html/html-parser.tspackages/kg-default-nodes/src/nodes/html/html-renderer.tspackages/kg-default-nodes/src/nodes/image/ImageNode.tspackages/kg-default-nodes/src/nodes/image/image-parser.tspackages/kg-default-nodes/src/nodes/image/image-renderer.tspackages/kg-default-nodes/src/nodes/markdown/MarkdownNode.tspackages/kg-default-nodes/src/nodes/markdown/markdown-renderer.tspackages/kg-default-nodes/src/nodes/paywall/PaywallNode.tspackages/kg-default-nodes/src/nodes/paywall/paywall-parser.tspackages/kg-default-nodes/src/nodes/paywall/paywall-renderer.tspackages/kg-default-nodes/src/nodes/product/ProductNode.tspackages/kg-default-nodes/src/nodes/product/product-parser.tspackages/kg-default-nodes/src/nodes/product/product-renderer.tspackages/kg-default-nodes/src/nodes/signup/SignupNode.tspackages/kg-default-nodes/src/nodes/signup/signup-parser.tspackages/kg-default-nodes/src/nodes/signup/signup-renderer.tspackages/kg-default-nodes/src/nodes/toggle/ToggleNode.tspackages/kg-default-nodes/src/nodes/toggle/toggle-parser.tspackages/kg-default-nodes/src/nodes/toggle/toggle-renderer.tspackages/kg-default-nodes/src/nodes/transistor/TransistorNode.tspackages/kg-default-nodes/src/nodes/transistor/transistor-renderer.tspackages/kg-default-nodes/src/nodes/video/VideoNode.tspackages/kg-default-nodes/src/nodes/video/video-parser.tspackages/kg-default-nodes/src/nodes/video/video-renderer.tspackages/kg-default-nodes/src/nodes/zwnj/ZWNJNode.tspackages/kg-default-nodes/src/serializers/linebreak.tspackages/kg-default-nodes/src/serializers/paragraph.tspackages/kg-default-nodes/src/svg.d.tspackages/kg-default-nodes/src/utils/add-create-document-option.tspackages/kg-default-nodes/src/utils/build-clean-basic-html-for-element.tspackages/kg-default-nodes/src/utils/clean-dom.tspackages/kg-default-nodes/src/utils/escape-html.tspackages/kg-default-nodes/src/utils/get-available-image-widths.tspackages/kg-default-nodes/src/utils/get-resized-image-dimensions.tspackages/kg-default-nodes/src/utils/is-local-content-image.tspackages/kg-default-nodes/src/utils/is-unsplash-image.tspackages/kg-default-nodes/src/utils/read-caption-from-element.tspackages/kg-default-nodes/src/utils/read-image-attributes-from-element.tspackages/kg-default-nodes/src/utils/read-text-content.tspackages/kg-default-nodes/src/utils/render-empty-container.tspackages/kg-default-nodes/src/utils/render-helpers/email-button.tspackages/kg-default-nodes/src/utils/replacement-strings.tspackages/kg-default-nodes/src/utils/rgb-to-hex.tspackages/kg-default-nodes/src/utils/set-src-background-from-parent.tspackages/kg-default-nodes/src/utils/size-byte-converter.tspackages/kg-default-nodes/src/utils/slugify.tspackages/kg-default-nodes/src/utils/srcset-attribute.tspackages/kg-default-nodes/src/utils/tagged-template-fns.tspackages/kg-default-nodes/src/utils/truncate.tspackages/kg-default-nodes/src/utils/visibility.tspackages/kg-default-nodes/test/generate-decorator-node.test.tspackages/kg-default-nodes/test/nodes/aside.test.tspackages/kg-default-nodes/test/nodes/at-link-search.test.tspackages/kg-default-nodes/test/nodes/at-link.test.tspackages/kg-default-nodes/test/nodes/audio.test.tspackages/kg-default-nodes/test/nodes/bookmark.test.tspackages/kg-default-nodes/test/nodes/button.test.tspackages/kg-default-nodes/test/nodes/call-to-action.test.tspackages/kg-default-nodes/test/nodes/callout.test.tspackages/kg-default-nodes/test/nodes/codeblock.test.tspackages/kg-default-nodes/test/nodes/email-cta.test.tspackages/kg-default-nodes/test/nodes/email.test.tspackages/kg-default-nodes/test/nodes/embed.test.tspackages/kg-default-nodes/test/nodes/file.test.tspackages/kg-default-nodes/test/nodes/gallery.test.tspackages/kg-default-nodes/test/nodes/header.test.tspackages/kg-default-nodes/test/nodes/horizontalrule.test.tspackages/kg-default-nodes/test/nodes/html.test.tspackages/kg-default-nodes/test/nodes/image.test.tspackages/kg-default-nodes/test/nodes/markdown.test.tspackages/kg-default-nodes/test/nodes/paywall.test.tspackages/kg-default-nodes/test/nodes/product.test.tspackages/kg-default-nodes/test/nodes/signup.test.tspackages/kg-default-nodes/test/nodes/tk.test.tspackages/kg-default-nodes/test/nodes/toggle.test.tspackages/kg-default-nodes/test/nodes/transistor.test.tspackages/kg-default-nodes/test/nodes/video.test.tspackages/kg-default-nodes/test/nodes/zwnj.test.tspackages/kg-default-nodes/test/serializers/linebreak.test.tspackages/kg-default-nodes/test/serializers/paragraph.test.tspackages/kg-default-nodes/test/test-utils/assertions.jspackages/kg-default-nodes/test/test-utils/assertions.tspackages/kg-default-nodes/test/test-utils/html-minifier.d.tspackages/kg-default-nodes/test/test-utils/index.jspackages/kg-default-nodes/test/test-utils/index.tspackages/kg-default-nodes/test/test-utils/overrides.jspackages/kg-default-nodes/test/test-utils/overrides.tspackages/kg-default-nodes/test/test-utils/should-assertions.d.tspackages/kg-default-nodes/test/test-utils/should.d.tspackages/kg-default-nodes/test/utils/rgb-to-hex.test.tspackages/kg-default-nodes/test/utils/tagged-template-fns.test.tspackages/kg-default-nodes/test/utils/visibility.test.tspackages/kg-default-nodes/tsconfig.cjs.jsonpackages/kg-default-nodes/tsconfig.jsonpackages/kg-default-nodes/tsconfig.test.json
💤 Files with no reviewable changes (14)
- packages/kg-default-nodes/index.js
- packages/kg-default-nodes/lib/nodes/at-link/index.js
- packages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-parser.js
- packages/kg-default-nodes/lib/nodes/markdown/markdown-renderer.js
- packages/kg-default-nodes/lib/nodes/paywall/paywall-parser.js
- packages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-renderer.js
- packages/kg-default-nodes/lib/utils/is-unsplash-image.js
- packages/kg-default-nodes/lib/nodes/html/html-parser.js
- packages/kg-default-nodes/rollup.config.mjs
- packages/kg-default-nodes/test/test-utils/overrides.js
- packages/kg-default-nodes/test/test-utils/index.js
- packages/kg-default-nodes/test/test-utils/assertions.js
- packages/kg-default-nodes/lib/nodes/codeblock/codeblock-parser.js
- packages/kg-default-nodes/lib/kg-default-nodes.js
| export const isUnsplashImage = function (url: string) { | ||
| return /images\.unsplash\.com/.test(url); |
There was a problem hiding this comment.
Use hostname validation instead of substring matching.
This regex matches anywhere in the string, so non-Unsplash URLs can be misclassified if they contain images.unsplash.com in query/path text. Parse the URL and validate hostname directly.
Proposed fix
export const isUnsplashImage = function (url: string) {
- return /images\.unsplash\.com/.test(url);
+ try {
+ return new URL(url).hostname === 'images.unsplash.com';
+ } catch {
+ return false;
+ }
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const isUnsplashImage = function (url: string) { | |
| return /images\.unsplash\.com/.test(url); | |
| export const isUnsplashImage = function (url: string) { | |
| try { | |
| return new URL(url).hostname === 'images.unsplash.com'; | |
| } catch { | |
| return false; | |
| } | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/kg-default-nodes/src/utils/is-unsplash-image.ts` around lines 1 - 2,
Replace the substring/regex check in isUnsplashImage with proper URL hostname
validation: in the isUnsplashImage function, attempt to construct a new URL(url)
inside a try/catch and return false for invalid URLs, then compare
urlObj.hostname strictly to "images.unsplash.com" (or use an appropriate
endsWith check if you want subdomains) instead of testing the string with
/images\.unsplash\.com/ so only actual hosts match.
ef572c1 to
25f1493
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (4)
packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts (2)
180-182:⚠️ Potential issue | 🔴 CriticalAvoid unsanitized
innerHTMLfor captions.Line 181 writes
node.captiondirectly toinnerHTML, which is an XSS sink. Sanitize with the shared allowlist utility (preferred) or fall back totextContentif markup is not required.Minimal safe fallback
- figcaption.innerHTML = node.caption; + figcaption.textContent = node.caption;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts` around lines 180 - 182, Replace the unsafe figcaption.innerHTML = node.caption assignment in gallery-renderer.ts: import and use the shared allowlist sanitizer utility to sanitize node.caption before setting innerHTML on the created figcaption element (e.g., sanitized = allowlistSanitize(node.caption); figcaption.innerHTML = sanitized), and if the allowlist sanitizer is not available or returns null/empty, fall back to assigning the plain text via figcaption.textContent = node.caption; ensure the sanitizer function is referenced and imported where the figcaption is created and handle undefined/null captions safely.
145-147:⚠️ Potential issue | 🟠 MajorGuard
contentImageSizesbefore retina-size lookup.Line 147 dereferences
options.imageOptimization!.contentImageSizes!even though both are optional inGalleryRenderOptions; this can throw at runtime in email rendering when transform is enabled without size metadata.Suggested fix
- if (isLocalContentImage(image.src, options.siteUrl) && options.canTransformImage && options.canTransformImage(image.src)) { + const contentImageSizes = options.imageOptimization?.contentImageSizes; + if (contentImageSizes && isLocalContentImage(image.src, options.siteUrl) && options.canTransformImage?.(image.src)) { // find available image size next up from 2x600 so we can use it for the "retina" src - const availableImageWidths = getAvailableImageWidths(image, options.imageOptimization!.contentImageSizes!); + const availableImageWidths = getAvailableImageWidths(image, contentImageSizes); const srcWidth = availableImageWidths.find(width => width >= 1200);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts` around lines 145 - 147, The code in GalleryRenderOptions assumes options.imageOptimization and contentImageSizes exist when calling getAvailableImageWidths(image, options.imageOptimization!.contentImageSizes!), which can throw; update the gallery-renderer.ts flow (around the block using isLocalContentImage(image.src, options.siteUrl) and getAvailableImageWidths) to first check options.imageOptimization and options.imageOptimization.contentImageSizes are defined before performing the retina-size lookup, and if missing skip or short-circuit the retina logic (use the non-retina src fallback) so getAvailableImageWidths is never called with undefined; reference the image variable, options, getAvailableImageWidths and the retina-size lookup in your change.packages/kg-default-nodes/src/nodes/html/html-parser.ts (1)
12-18:⚠️ Potential issue | 🟠 MajorUnsafe cast to
Element— text/comment nodes lackouterHTML.The loop assumes all siblings are Elements, but text nodes (whitespace, line breaks) or other comment nodes between the markers would cause
outerHTMLto returnundefined, polluting the HTML output with "undefined" strings.This issue was flagged in a previous review but appears unresolved.
🐛 Proposed fix
while (nextNode && !isHtmlEndComment(nextNode)) { const currentNode = nextNode; - html.push((currentNode as Element).outerHTML); + if (currentNode.nodeType === 1) { + html.push((currentNode as Element).outerHTML); + } nextNode = currentNode.nextSibling; // remove nodes as we go so that they don't go through the parser currentNode.remove(); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/html/html-parser.ts` around lines 12 - 18, The loop in html-parser.ts assumes every sibling is an Element and uses (currentNode as Element).outerHTML, which produces "undefined" for text/comment nodes; update the while loop that uses nextNode/currentNode and isHtmlEndComment to guard by node type: if currentNode.nodeType === Node.ELEMENT_NODE (or currentNode instanceof Element) push its outerHTML, else if currentNode.nodeType === Node.TEXT_NODE push its textContent (trim or skip if only whitespace), and skip comment nodes (Node.COMMENT_NODE); keep removing currentNode after handling it so nodes still don't reach the parser.packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts (1)
401-416:⚠️ Potential issue | 🟠 MajorSanitize
sponsorLabelbefore the email early-return.The email rendering path (lines 401-408) returns before
sponsorLabelis sanitized (lines 412-416). This means the email template at line 405 receives raw, unsanitizedsponsorLabelcontent, while the web template gets sanitized content.Move the sanitization before the email branch:
🐛 Proposed fix
+ if (dataset.hasSponsorLabel) { + const cleanBasicHtml = buildCleanBasicHtmlForElement(document.createElement('div')); + const cleanedHtml = cleanBasicHtml(dataset.sponsorLabel, {firstChildInnerContent: true}); + dataset.sponsorLabel = cleanedHtml || ''; + } + if (options.target === 'email') { const emailDoc = options.createDocument!(); const emailDiv = emailDoc.createElement('div'); emailDiv.innerHTML = emailCTATemplate(dataset, options); return renderWithVisibility({element: emailDiv.firstElementChild as RenderOutput['element']}, node.visibility as Visibility, options); } const element = document.createElement('div'); - if (dataset.hasSponsorLabel) { - const cleanBasicHtml = buildCleanBasicHtmlForElement(element); - const cleanedHtml = cleanBasicHtml(dataset.sponsorLabel, {firstChildInnerContent: true}); - dataset.sponsorLabel = cleanedHtml || ''; - } const htmlString = ctaCardTemplate(dataset);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts` around lines 401 - 416, The email rendering path returns before sanitizing dataset.sponsorLabel, so the emailCTATemplate receives unsanitized HTML; move the sponsorLabel sanitization (use buildCleanBasicHtmlForElement and cleanBasicHtml) to occur before the options.target === 'email' branch and assign the sanitized string back to dataset.sponsorLabel, then proceed to call emailCTATemplate and renderWithVisibility (preserving node.visibility and options) so both email and web paths use the cleaned sponsorLabel.
🧹 Nitpick comments (10)
packages/kg-default-nodes/src/nodes/ExtendedHeadingNode.ts (1)
47-50: ConsistentDOMConverterFntype pattern.This type definition mirrors the one in
ExtendedTextNode.ts. Consider extracting this shared type to a common utilities module if more extended nodes are added in the future.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/ExtendedHeadingNode.ts` around lines 47 - 50, The DOMConverterFn type is duplicated (see DOMConverterFn and patchParagraphConversion) and should be extracted to a shared utility to keep types consistent across extended nodes; create a new exported type (e.g., DOMConverterFn) in a common utils/types module, replace the inline DOMConverterFn declaration in this file and in ExtendedTextNode by importing that shared type, and update any references in patchParagraphConversion and related conversion functions to use the imported type.packages/kg-default-nodes/src/nodes/product/product-renderer.ts (2)
54-61: Consider the innerHTML security context.The static analysis flags the
innerHTMLassignment as a potential XSS vector. In this CMS renderer context, the risk is mitigated because:
- Data originates from the Ghost editor/database (trusted input)
- Content sanitization typically occurs at write time in CMS architectures
However, if any of the template data fields (
productUrl,productTitle,productButton,productDescription,productImageSrc) could contain user-controlled content that bypasses editor sanitization, this would be exploitable.If stricter security is desired, consider sanitizing URL attributes with a URL validator or encoding HTML entities for text content.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/product/product-renderer.ts` around lines 54 - 61, The assignment to element.innerHTML using htmlString (produced by emailCardTemplate or cardTemplate) is flagged as a potential XSS vector; ensure template data fields (productUrl, productTitle, productButton, productDescription, productImageSrc) are sanitized/validated before rendering by either validating/normalizing URLs (e.g., allow only http(s) and data:image for productImageSrc/productUrl) and HTML-encoding any free-text fields, or by switching to safe DOM creation rather than innerHTML; update the rendering flow in product-renderer.ts so that htmlString is produced from sanitized templateData (or replace element.innerHTML usage with element.appendChild/fromDocumentFragment built using createElement/setAttribute/textContent) while keeping selection between emailCardTemplate and cardTemplate.
96-105: Type assertions are acceptable but consider stronger typing in future.The
as numberassertions on lines 98-99 and 102 are necessary because the truthiness check doesn't narrowRecord<string, unknown>to a specific type. This pattern is common when working with loosely-typed data structures.For future improvement, consider defining a more specific interface for template data (e.g.,
ProductTemplateDatawithproductImageWidth?: number) to eliminate the need for runtime type assertions. This would be a broader refactor across renderers.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/product/product-renderer.ts` around lines 96 - 105, The code uses runtime type assertions (data.productImageWidth as number, data.productImageHeight as number) because Record<string, unknown> prevents proper type narrowing; to fix, introduce a stronger type for the template data (e.g., interface ProductTemplateData { productImageWidth?: number; productImageHeight?: number; ... }) and annotate the renderer function parameter so `data` is ProductTemplateData, then remove the `as number` casts and rely on the existing truthiness checks before using `imageDimensions` and calling getResizedImageDimensions; alternatively, if changing the parameter type is not possible right now, add a small type guard function (e.g., isNumber) and use it to narrow data.productImageWidth/data.productImageHeight before assigning to imageDimensions to avoid unsafe assertions.packages/kg-default-nodes/src/nodes/signup/SignupNode.ts (1)
108-110: Consider tightening the parameter type for better type safety.The function accepts
Record<string, unknown>but passes it to a constructor expectingSignupData. This type mismatch allows invalid property types to slip through at compile time (e.g.,labelsas a non-array).If the loose type is intentional for parsing flexibility from external sources, consider documenting it or adding minimal runtime validation for critical fields.
💡 Option: Use SignupData for stricter typing
-export const $createSignupNode = (dataset: Record<string, unknown>) => { +export const $createSignupNode = (dataset: SignupData) => { return new SignupNode(dataset); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/signup/SignupNode.ts` around lines 108 - 110, The factory $createSignupNode currently types its parameter as Record<string, unknown> but immediately passes it to new SignupNode which expects SignupData; tighten the parameter to SignupData (replace the dataset type) to ensure compile-time safety, or if loose parsing is required, add minimal runtime validation inside $createSignupNode to coerce/verify critical fields (e.g., ensure labels is an array) and document why Record<string, unknown> is used; update references to SignupData, $createSignupNode, and SignupNode accordingly so the constructor always receives a valid SignupData object.packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts (1)
114-116: Consider stricter parameter typing for$createBookmarkNode.The function accepts
Record<string, unknown>but passes it to a constructor expectingBookmarkData. This reduces type safety—callers can pass any shape without compile-time validation.If you want to preserve flexibility for deserialization scenarios while improving type safety for direct usage:
♻️ Suggested improvement
-export const $createBookmarkNode = (dataset: Record<string, unknown>) => { +export const $createBookmarkNode = (dataset: BookmarkData) => { return new BookmarkNode(dataset); };Callers with
Record<string, unknown>data can cast explicitly:$createBookmarkNode(data as BookmarkData).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts` around lines 114 - 116, The $createBookmarkNode function currently accepts a loose Record<string, unknown> but passes it to the BookmarkNode constructor which expects BookmarkData; tighten the signature to accept BookmarkData (or BookmarkData | Record<string, unknown> if you must preserve deserialization flexibility) and, if allowing the union, perform an explicit cast/validation before calling new BookmarkNode(dataset) so callers get compile-time safety; update the $createBookmarkNode parameter type and any callers that construct nodes from known BookmarkData to pass BookmarkData (or cast deserialized objects with `as BookmarkData`) and ensure the constructor argument to BookmarkNode is the properly typed BookmarkData.packages/kg-default-nodes/src/nodes/markdown/markdown-renderer.ts (1)
8-13: The recommendation to use the renderer's exported options type is not feasible. The@tryghost/kg-markdown-html-rendererpackage does not export a TypeScript options type—RenderOptionsis internal and inaccessible. The cast toRecord<string, unknown>on line 19 is necessary becauseMarkdownRenderOptionsincludes properties (createDocument,dom,target) that therender()function doesn't accept (it only supportsghostVersion). A better alternative would be to extract only the relevant options before passing them torender(), or to acknowledge the type boundary and document the cast intent with a comment.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/markdown/markdown-renderer.ts` around lines 8 - 13, The custom MarkdownRenderOptions includes createDocument, dom, and target which aren't accepted by the external render() (internal RenderOptions only accepts ghostVersion), so remove the unsafe broad cast: extract only the supported options (e.g., const { ghostVersion } = options) or build a new object containing just the allowed keys before calling render(), and update the call site that currently casts to Record<string, unknown> to pass this filtered object; if you must keep the cast, replace it with a short comment explaining the intentional type boundary and why createDocument/dom/target are excluded.packages/kg-default-nodes/test/test-utils/index.ts (1)
6-16: Consider adding error handling for Prettier formatting.
Prettier.format()can throw if the input HTML is severely malformed. While test inputs are typically controlled, wrapping this in a try-catch or documenting this behavior would improve robustness.That said, for test utilities where inputs are controlled, this is acceptable as-is.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/test-utils/index.ts` around lines 6 - 16, The html test helper calls Prettier.format(output, {parser: 'html'}) which can throw on malformed input; wrap the Prettier.format call in a try-catch inside the html function (or explicitly document the potential exception) so tests don't crash on formatting errors—on catch, return the raw output or a safe fallback and optionally log or rethrow with additional context mentioning the html helper and the failing output so the failure is clear.packages/kg-default-nodes/test/nodes/paywall.test.ts (1)
90-94: Consider a more targeted type assertion forexportDOM.The
as unknown as LexicalEditorcast is a broad escape hatch. TheexportDOMmethod signature expects aLexicalEditor, but you're passing render options. This works at runtime because the decorator node'sexportDOMimplementation uses these options, but the type mismatch suggests the base type definition may need adjustment upstream.For now, this is acceptable as a migration workaround, but consider creating a dedicated type or interface for export options to avoid the double cast pattern across test files.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/paywall.test.ts` around lines 90 - 94, The test is using a broad double-cast (as unknown as LexicalEditor) when calling paywallNode.exportDOM; instead create a narrow mock or interface that matches what exportDOM actually reads instead of pretending to be a full LexicalEditor: either define a small ExportDOMOptions/RenderExportOptions type that describes the properties exportDOM uses and cast exportOptions to that, or construct a minimal mock object implementing just the methods/properties the $createPaywallNode().exportDOM(exportOptions) implementation accesses, then pass that instead of casting to LexicalEditor; update the test to call exportDOM with this targeted mock/interface so you remove the double-cast while keeping behavior unchanged.packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts (1)
136-142: Non-null assertion onmatch()at line 138 may throw if regex doesn't match.The
!assertion assumes the regex will always match. WhileisLocalContentImagelikely validates that the URL contains/content/images/, if that assumption is violated, this will throw a runtime error.Consider adding a defensive check:
♻️ Suggested defensive fix
if (isLocalContentImage(dataset.imageUrl, options.siteUrl) && options.canTransformImage?.(dataset.imageUrl)) { - const [, imagesPath, filename] = dataset.imageUrl.match(/(.*\/content\/images)\/(.*)/)!; + const match = dataset.imageUrl.match(/(.*\/content\/images)\/(.*)/); + if (!match) { + // URL doesn't match expected format, skip transformation + return; + } + const [, imagesPath, filename] = match; const iconSize = options?.imageOptimization?.internalImageSizes?.['email-cta-minimal-image'] || {width: 256, height: 256}; dataset.imageUrl = `${imagesPath}/size/w${iconSize.width}h${iconSize.height}/${filename}`; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts` around lines 136 - 142, The code uses a non-null assertion on dataset.imageUrl.match(...) inside the block that checks isLocalContentImage(...) and options.canTransformImage(...), which can still throw if the regex doesn't match; update the transformation in calltoaction-renderer.ts to first assign the match result to a variable, verify it's not null before destructuring (or bail out/leave dataset.imageUrl unchanged), and only build the optimized URL when the match succeeds; reference the dataset.imageUrl match, the isLocalContentImage(...) guard, and the iconSize calculation so the logic flow remains the same but safe against missing regex matches.packages/kg-default-nodes/src/generate-decorator-node.ts (1)
241-253: Consider adding a type guard for versioned renderer lookup.The version-based renderer lookup uses
numberindexing butnodeVersioncould be a number or come fromthis.__version. The current castas numberworks but the logic assumes version is always a valid key.💡 Optional: Add defensive check for missing versioned renderer
The current code throws a helpful error when the versioned renderer is missing, which is good. The implementation handles this case properly.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/generate-decorator-node.ts` around lines 241 - 253, The node renderer lookup assumes nodeVersion is a valid numeric key; add a type guard and normalize nodeVersion before indexing into nodeRenderers: retrieve nodeVersion from the local variable or this.__version, coerce/validate it to a finite number or string key (e.g., ensure typeof nodeVersion === 'number' && Number.isFinite(nodeVersion) or convert from string safely), then use that normalized key when accessing (render as Record<string|number,RenderFn>)[normalizedVersion]; keep the existing error throw if the versioned renderer is missing and update references to nodeVersion/this.__version and the local render variable in generateDecoratorNode to use the validated normalizedVersion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/kg-default-nodes/package.json`:
- Around line 19-21: The package.json scripts ("build", "prepare", "pretest")
run tsc but do not clean stale artifacts; modify each script to remove/clean the
build directory before running tsc (e.g., run a cross-platform cleaner like
rimraf build or rm -rf build && mkdir -p build) so old JS/.d.ts files are
deleted before emitting, then run the existing tsc commands and the echo step;
update "build", "prepare", and "pretest" to include this pre-clean step (or add
a separate "clean" script and invoke it from those scripts).
In `@packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts`:
- Around line 51-55: The code in the isEmail && isVideoWithThumbnail branch uses
non-null assertions on metadata.thumbnail_width and metadata.thumbnail_height
(line computing thumbnailAspectRatio) which can be missing or zero and lead to
division-by-zero or invalid spacerHeight; update the guard in that block to
verify metadata.thumbnail_width and metadata.thumbnail_height are present and >
0 before computing thumbnailAspectRatio and spacerHeight, and if they are
absent/invalid either skip thumbnail sizing logic (avoid computing spacerHeight)
or use a safe default aspect ratio (e.g., 16/9) so spacerHeight and spacerWidth
are always valid; adjust the calculations that set spacerHeight/spacerWidth (and
any downstream use) accordingly to reference the validated values.
In `@packages/kg-default-nodes/src/nodes/markdown/markdown-renderer.ts`:
- Around line 19-23: The code assigns unsanitized HTML from
render(node.markdown) directly to element.innerHTML in markdown-renderer.ts,
creating an XSS risk; fix by sanitizing the rendered HTML (e.g., run the output
of render(...) through a sanitizer like DOMPurify before assigning to
element.innerHTML) or, if only plain text is needed, set element.textContent to
node.markdown instead; update the code paths around render, node.markdown and
element.innerHTML to use the sanitizer API (or textContent) and add a short
comment documenting the trust assumption if you opt to keep raw HTML.
In `@packages/kg-default-nodes/src/nodes/video/video-renderer.ts`:
- Around line 16-21: The VideoRenderOptions interface currently makes postUrl
optional which allows target === 'email' with postUrl undefined; change
VideoRenderOptions to a discriminated union (e.g., one variant where target:
'email' and postUrl: string, and another for other targets where postUrl?:
string) so TypeScript enforces postUrl when target is 'email'; update any
references or function signatures that accept VideoRenderOptions (e.g., the
function that uses emailCardTemplate) to use the new union type so the places
that interpolate postUrl into href must have a defined string.
---
Duplicate comments:
In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts`:
- Around line 401-416: The email rendering path returns before sanitizing
dataset.sponsorLabel, so the emailCTATemplate receives unsanitized HTML; move
the sponsorLabel sanitization (use buildCleanBasicHtmlForElement and
cleanBasicHtml) to occur before the options.target === 'email' branch and assign
the sanitized string back to dataset.sponsorLabel, then proceed to call
emailCTATemplate and renderWithVisibility (preserving node.visibility and
options) so both email and web paths use the cleaned sponsorLabel.
In `@packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts`:
- Around line 180-182: Replace the unsafe figcaption.innerHTML = node.caption
assignment in gallery-renderer.ts: import and use the shared allowlist sanitizer
utility to sanitize node.caption before setting innerHTML on the created
figcaption element (e.g., sanitized = allowlistSanitize(node.caption);
figcaption.innerHTML = sanitized), and if the allowlist sanitizer is not
available or returns null/empty, fall back to assigning the plain text via
figcaption.textContent = node.caption; ensure the sanitizer function is
referenced and imported where the figcaption is created and handle
undefined/null captions safely.
- Around line 145-147: The code in GalleryRenderOptions assumes
options.imageOptimization and contentImageSizes exist when calling
getAvailableImageWidths(image, options.imageOptimization!.contentImageSizes!),
which can throw; update the gallery-renderer.ts flow (around the block using
isLocalContentImage(image.src, options.siteUrl) and getAvailableImageWidths) to
first check options.imageOptimization and
options.imageOptimization.contentImageSizes are defined before performing the
retina-size lookup, and if missing skip or short-circuit the retina logic (use
the non-retina src fallback) so getAvailableImageWidths is never called with
undefined; reference the image variable, options, getAvailableImageWidths and
the retina-size lookup in your change.
In `@packages/kg-default-nodes/src/nodes/html/html-parser.ts`:
- Around line 12-18: The loop in html-parser.ts assumes every sibling is an
Element and uses (currentNode as Element).outerHTML, which produces "undefined"
for text/comment nodes; update the while loop that uses nextNode/currentNode and
isHtmlEndComment to guard by node type: if currentNode.nodeType ===
Node.ELEMENT_NODE (or currentNode instanceof Element) push its outerHTML, else
if currentNode.nodeType === Node.TEXT_NODE push its textContent (trim or skip if
only whitespace), and skip comment nodes (Node.COMMENT_NODE); keep removing
currentNode after handling it so nodes still don't reach the parser.
---
Nitpick comments:
In `@packages/kg-default-nodes/src/generate-decorator-node.ts`:
- Around line 241-253: The node renderer lookup assumes nodeVersion is a valid
numeric key; add a type guard and normalize nodeVersion before indexing into
nodeRenderers: retrieve nodeVersion from the local variable or this.__version,
coerce/validate it to a finite number or string key (e.g., ensure typeof
nodeVersion === 'number' && Number.isFinite(nodeVersion) or convert from string
safely), then use that normalized key when accessing (render as
Record<string|number,RenderFn>)[normalizedVersion]; keep the existing error
throw if the versioned renderer is missing and update references to
nodeVersion/this.__version and the local render variable in
generateDecoratorNode to use the validated normalizedVersion.
In `@packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts`:
- Around line 114-116: The $createBookmarkNode function currently accepts a
loose Record<string, unknown> but passes it to the BookmarkNode constructor
which expects BookmarkData; tighten the signature to accept BookmarkData (or
BookmarkData | Record<string, unknown> if you must preserve deserialization
flexibility) and, if allowing the union, perform an explicit cast/validation
before calling new BookmarkNode(dataset) so callers get compile-time safety;
update the $createBookmarkNode parameter type and any callers that construct
nodes from known BookmarkData to pass BookmarkData (or cast deserialized objects
with `as BookmarkData`) and ensure the constructor argument to BookmarkNode is
the properly typed BookmarkData.
In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts`:
- Around line 136-142: The code uses a non-null assertion on
dataset.imageUrl.match(...) inside the block that checks
isLocalContentImage(...) and options.canTransformImage(...), which can still
throw if the regex doesn't match; update the transformation in
calltoaction-renderer.ts to first assign the match result to a variable, verify
it's not null before destructuring (or bail out/leave dataset.imageUrl
unchanged), and only build the optimized URL when the match succeeds; reference
the dataset.imageUrl match, the isLocalContentImage(...) guard, and the iconSize
calculation so the logic flow remains the same but safe against missing regex
matches.
In `@packages/kg-default-nodes/src/nodes/ExtendedHeadingNode.ts`:
- Around line 47-50: The DOMConverterFn type is duplicated (see DOMConverterFn
and patchParagraphConversion) and should be extracted to a shared utility to
keep types consistent across extended nodes; create a new exported type (e.g.,
DOMConverterFn) in a common utils/types module, replace the inline
DOMConverterFn declaration in this file and in ExtendedTextNode by importing
that shared type, and update any references in patchParagraphConversion and
related conversion functions to use the imported type.
In `@packages/kg-default-nodes/src/nodes/markdown/markdown-renderer.ts`:
- Around line 8-13: The custom MarkdownRenderOptions includes createDocument,
dom, and target which aren't accepted by the external render() (internal
RenderOptions only accepts ghostVersion), so remove the unsafe broad cast:
extract only the supported options (e.g., const { ghostVersion } = options) or
build a new object containing just the allowed keys before calling render(), and
update the call site that currently casts to Record<string, unknown> to pass
this filtered object; if you must keep the cast, replace it with a short comment
explaining the intentional type boundary and why createDocument/dom/target are
excluded.
In `@packages/kg-default-nodes/src/nodes/product/product-renderer.ts`:
- Around line 54-61: The assignment to element.innerHTML using htmlString
(produced by emailCardTemplate or cardTemplate) is flagged as a potential XSS
vector; ensure template data fields (productUrl, productTitle, productButton,
productDescription, productImageSrc) are sanitized/validated before rendering by
either validating/normalizing URLs (e.g., allow only http(s) and data:image for
productImageSrc/productUrl) and HTML-encoding any free-text fields, or by
switching to safe DOM creation rather than innerHTML; update the rendering flow
in product-renderer.ts so that htmlString is produced from sanitized
templateData (or replace element.innerHTML usage with
element.appendChild/fromDocumentFragment built using
createElement/setAttribute/textContent) while keeping selection between
emailCardTemplate and cardTemplate.
- Around line 96-105: The code uses runtime type assertions
(data.productImageWidth as number, data.productImageHeight as number) because
Record<string, unknown> prevents proper type narrowing; to fix, introduce a
stronger type for the template data (e.g., interface ProductTemplateData {
productImageWidth?: number; productImageHeight?: number; ... }) and annotate the
renderer function parameter so `data` is ProductTemplateData, then remove the
`as number` casts and rely on the existing truthiness checks before using
`imageDimensions` and calling getResizedImageDimensions; alternatively, if
changing the parameter type is not possible right now, add a small type guard
function (e.g., isNumber) and use it to narrow
data.productImageWidth/data.productImageHeight before assigning to
imageDimensions to avoid unsafe assertions.
In `@packages/kg-default-nodes/src/nodes/signup/SignupNode.ts`:
- Around line 108-110: The factory $createSignupNode currently types its
parameter as Record<string, unknown> but immediately passes it to new SignupNode
which expects SignupData; tighten the parameter to SignupData (replace the
dataset type) to ensure compile-time safety, or if loose parsing is required,
add minimal runtime validation inside $createSignupNode to coerce/verify
critical fields (e.g., ensure labels is an array) and document why
Record<string, unknown> is used; update references to SignupData,
$createSignupNode, and SignupNode accordingly so the constructor always receives
a valid SignupData object.
In `@packages/kg-default-nodes/test/nodes/paywall.test.ts`:
- Around line 90-94: The test is using a broad double-cast (as unknown as
LexicalEditor) when calling paywallNode.exportDOM; instead create a narrow mock
or interface that matches what exportDOM actually reads instead of pretending to
be a full LexicalEditor: either define a small
ExportDOMOptions/RenderExportOptions type that describes the properties
exportDOM uses and cast exportOptions to that, or construct a minimal mock
object implementing just the methods/properties the
$createPaywallNode().exportDOM(exportOptions) implementation accesses, then pass
that instead of casting to LexicalEditor; update the test to call exportDOM with
this targeted mock/interface so you remove the double-cast while keeping
behavior unchanged.
In `@packages/kg-default-nodes/test/test-utils/index.ts`:
- Around line 6-16: The html test helper calls Prettier.format(output, {parser:
'html'}) which can throw on malformed input; wrap the Prettier.format call in a
try-catch inside the html function (or explicitly document the potential
exception) so tests don't crash on formatting errors—on catch, return the raw
output or a safe fallback and optionally log or rethrow with additional context
mentioning the html helper and the failing output so the failure is clear.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 135dc1ea-e8d5-49ae-bf9a-a3a3d15f4faf
📒 Files selected for processing (150)
packages/kg-default-nodes/eslint.config.mjspackages/kg-default-nodes/index.jspackages/kg-default-nodes/package.jsonpackages/kg-default-nodes/rollup.config.mjspackages/kg-default-nodes/src/KoenigDecoratorNode.tspackages/kg-default-nodes/src/generate-decorator-node.tspackages/kg-default-nodes/src/index.tspackages/kg-default-nodes/src/kg-default-nodes.tspackages/kg-default-nodes/src/nodes/ExtendedHeadingNode.tspackages/kg-default-nodes/src/nodes/ExtendedQuoteNode.tspackages/kg-default-nodes/src/nodes/ExtendedTextNode.tspackages/kg-default-nodes/src/nodes/TKNode.tspackages/kg-default-nodes/src/nodes/aside/AsideNode.tspackages/kg-default-nodes/src/nodes/aside/AsideParser.tspackages/kg-default-nodes/src/nodes/at-link/AtLinkNode.tspackages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.tspackages/kg-default-nodes/src/nodes/at-link/index.tspackages/kg-default-nodes/src/nodes/audio/AudioNode.tspackages/kg-default-nodes/src/nodes/audio/audio-parser.tspackages/kg-default-nodes/src/nodes/audio/audio-renderer.tspackages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.tspackages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.tspackages/kg-default-nodes/src/nodes/bookmark/bookmark-renderer.tspackages/kg-default-nodes/src/nodes/button/ButtonNode.tspackages/kg-default-nodes/src/nodes/button/button-parser.tspackages/kg-default-nodes/src/nodes/button/button-renderer.tspackages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.tspackages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.tspackages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.tspackages/kg-default-nodes/src/nodes/callout/CalloutNode.tspackages/kg-default-nodes/src/nodes/callout/callout-parser.tspackages/kg-default-nodes/src/nodes/callout/callout-renderer.tspackages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.tspackages/kg-default-nodes/src/nodes/codeblock/codeblock-parser.tspackages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.tspackages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.tspackages/kg-default-nodes/src/nodes/email-cta/email-cta-renderer.tspackages/kg-default-nodes/src/nodes/email/EmailNode.tspackages/kg-default-nodes/src/nodes/email/email-renderer.tspackages/kg-default-nodes/src/nodes/embed/EmbedNode.tspackages/kg-default-nodes/src/nodes/embed/embed-parser.tspackages/kg-default-nodes/src/nodes/embed/embed-renderer.tspackages/kg-default-nodes/src/nodes/embed/types/twitter.tspackages/kg-default-nodes/src/nodes/file/FileNode.tspackages/kg-default-nodes/src/nodes/file/file-parser.tspackages/kg-default-nodes/src/nodes/file/file-renderer.tspackages/kg-default-nodes/src/nodes/gallery/GalleryNode.tspackages/kg-default-nodes/src/nodes/gallery/gallery-parser.tspackages/kg-default-nodes/src/nodes/gallery/gallery-renderer.tspackages/kg-default-nodes/src/nodes/header/HeaderNode.tspackages/kg-default-nodes/src/nodes/header/parsers/header-parser.tspackages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.tspackages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.tspackages/kg-default-nodes/src/nodes/horizontalrule/HorizontalRuleNode.tspackages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-parser.tspackages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-renderer.tspackages/kg-default-nodes/src/nodes/html/HtmlNode.tspackages/kg-default-nodes/src/nodes/html/html-parser.tspackages/kg-default-nodes/src/nodes/html/html-renderer.tspackages/kg-default-nodes/src/nodes/image/ImageNode.tspackages/kg-default-nodes/src/nodes/image/image-parser.tspackages/kg-default-nodes/src/nodes/image/image-renderer.tspackages/kg-default-nodes/src/nodes/markdown/MarkdownNode.tspackages/kg-default-nodes/src/nodes/markdown/markdown-renderer.tspackages/kg-default-nodes/src/nodes/paywall/PaywallNode.tspackages/kg-default-nodes/src/nodes/paywall/paywall-parser.tspackages/kg-default-nodes/src/nodes/paywall/paywall-renderer.tspackages/kg-default-nodes/src/nodes/product/ProductNode.tspackages/kg-default-nodes/src/nodes/product/product-parser.tspackages/kg-default-nodes/src/nodes/product/product-renderer.tspackages/kg-default-nodes/src/nodes/signup/SignupNode.tspackages/kg-default-nodes/src/nodes/signup/signup-parser.tspackages/kg-default-nodes/src/nodes/signup/signup-renderer.tspackages/kg-default-nodes/src/nodes/toggle/ToggleNode.tspackages/kg-default-nodes/src/nodes/toggle/toggle-parser.tspackages/kg-default-nodes/src/nodes/toggle/toggle-renderer.tspackages/kg-default-nodes/src/nodes/transistor/TransistorNode.tspackages/kg-default-nodes/src/nodes/transistor/transistor-renderer.tspackages/kg-default-nodes/src/nodes/video/VideoNode.tspackages/kg-default-nodes/src/nodes/video/video-parser.tspackages/kg-default-nodes/src/nodes/video/video-renderer.tspackages/kg-default-nodes/src/nodes/zwnj/ZWNJNode.tspackages/kg-default-nodes/src/serializers/linebreak.tspackages/kg-default-nodes/src/serializers/paragraph.tspackages/kg-default-nodes/src/svg.d.tspackages/kg-default-nodes/src/utils/add-create-document-option.tspackages/kg-default-nodes/src/utils/build-clean-basic-html-for-element.tspackages/kg-default-nodes/src/utils/clean-dom.tspackages/kg-default-nodes/src/utils/escape-html.tspackages/kg-default-nodes/src/utils/get-available-image-widths.tspackages/kg-default-nodes/src/utils/get-resized-image-dimensions.tspackages/kg-default-nodes/src/utils/is-local-content-image.tspackages/kg-default-nodes/src/utils/is-unsplash-image.tspackages/kg-default-nodes/src/utils/read-caption-from-element.tspackages/kg-default-nodes/src/utils/read-image-attributes-from-element.tspackages/kg-default-nodes/src/utils/read-text-content.tspackages/kg-default-nodes/src/utils/render-empty-container.tspackages/kg-default-nodes/src/utils/render-helpers/email-button.tspackages/kg-default-nodes/src/utils/replacement-strings.tspackages/kg-default-nodes/src/utils/rgb-to-hex.tspackages/kg-default-nodes/src/utils/set-src-background-from-parent.tspackages/kg-default-nodes/src/utils/size-byte-converter.tspackages/kg-default-nodes/src/utils/slugify.tspackages/kg-default-nodes/src/utils/srcset-attribute.tspackages/kg-default-nodes/src/utils/tagged-template-fns.tspackages/kg-default-nodes/src/utils/truncate.tspackages/kg-default-nodes/src/utils/visibility.tspackages/kg-default-nodes/test/generate-decorator-node.test.tspackages/kg-default-nodes/test/nodes/aside.test.tspackages/kg-default-nodes/test/nodes/at-link-search.test.tspackages/kg-default-nodes/test/nodes/at-link.test.tspackages/kg-default-nodes/test/nodes/audio.test.tspackages/kg-default-nodes/test/nodes/bookmark.test.tspackages/kg-default-nodes/test/nodes/button.test.tspackages/kg-default-nodes/test/nodes/call-to-action.test.tspackages/kg-default-nodes/test/nodes/callout.test.tspackages/kg-default-nodes/test/nodes/codeblock.test.tspackages/kg-default-nodes/test/nodes/email-cta.test.tspackages/kg-default-nodes/test/nodes/email.test.tspackages/kg-default-nodes/test/nodes/embed.test.tspackages/kg-default-nodes/test/nodes/file.test.tspackages/kg-default-nodes/test/nodes/gallery.test.tspackages/kg-default-nodes/test/nodes/header.test.tspackages/kg-default-nodes/test/nodes/horizontalrule.test.tspackages/kg-default-nodes/test/nodes/html.test.tspackages/kg-default-nodes/test/nodes/image.test.tspackages/kg-default-nodes/test/nodes/markdown.test.tspackages/kg-default-nodes/test/nodes/paywall.test.tspackages/kg-default-nodes/test/nodes/product.test.tspackages/kg-default-nodes/test/nodes/signup.test.tspackages/kg-default-nodes/test/nodes/tk.test.tspackages/kg-default-nodes/test/nodes/toggle.test.tspackages/kg-default-nodes/test/nodes/transistor.test.tspackages/kg-default-nodes/test/nodes/video.test.tspackages/kg-default-nodes/test/nodes/zwnj.test.tspackages/kg-default-nodes/test/serializers/linebreak.test.tspackages/kg-default-nodes/test/serializers/paragraph.test.tspackages/kg-default-nodes/test/test-utils/assertions.tspackages/kg-default-nodes/test/test-utils/html-minifier.d.tspackages/kg-default-nodes/test/test-utils/index.tspackages/kg-default-nodes/test/test-utils/overrides.tspackages/kg-default-nodes/test/test-utils/should-assertions.d.tspackages/kg-default-nodes/test/test-utils/should.d.tspackages/kg-default-nodes/test/utils/rgb-to-hex.test.tspackages/kg-default-nodes/test/utils/tagged-template-fns.test.tspackages/kg-default-nodes/test/utils/visibility.test.tspackages/kg-default-nodes/tsconfig.cjs.jsonpackages/kg-default-nodes/tsconfig.jsonpackages/kg-default-nodes/tsconfig.test.jsonpackages/koenig-lexical/test/unit/hooks/useVisibilityToggle.test.js
💤 Files with no reviewable changes (2)
- packages/kg-default-nodes/index.js
- packages/kg-default-nodes/rollup.config.mjs
✅ Files skipped from review due to trivial changes (27)
- packages/kg-default-nodes/src/serializers/paragraph.ts
- packages/kg-default-nodes/src/utils/get-available-image-widths.ts
- packages/kg-default-nodes/src/utils/render-empty-container.ts
- packages/kg-default-nodes/src/utils/escape-html.ts
- packages/kg-default-nodes/test/utils/tagged-template-fns.test.ts
- packages/kg-default-nodes/src/index.ts
- packages/kg-default-nodes/src/utils/is-unsplash-image.ts
- packages/kg-default-nodes/src/nodes/at-link/index.ts
- packages/kg-default-nodes/src/utils/build-clean-basic-html-for-element.ts
- packages/kg-default-nodes/src/serializers/linebreak.ts
- packages/kg-default-nodes/src/svg.d.ts
- packages/kg-default-nodes/src/utils/slugify.ts
- packages/kg-default-nodes/src/nodes/zwnj/ZWNJNode.ts
- packages/kg-default-nodes/src/utils/render-helpers/email-button.ts
- packages/kg-default-nodes/src/nodes/paywall/paywall-parser.ts
- packages/kg-default-nodes/test/nodes/horizontalrule.test.ts
- packages/kg-default-nodes/tsconfig.json
- packages/kg-default-nodes/src/utils/truncate.ts
- packages/kg-default-nodes/tsconfig.test.json
- packages/kg-default-nodes/test/nodes/gallery.test.ts
- packages/kg-default-nodes/tsconfig.cjs.json
- packages/kg-default-nodes/src/nodes/paywall/PaywallNode.ts
- packages/kg-default-nodes/test/nodes/codeblock.test.ts
- packages/kg-default-nodes/src/nodes/callout/callout-parser.ts
- packages/kg-default-nodes/test/nodes/header.test.ts
- packages/kg-default-nodes/test/nodes/html.test.ts
- packages/kg-default-nodes/src/nodes/embed/types/twitter.ts
🚧 Files skipped from review as they are similar to previous changes (90)
- packages/kg-default-nodes/src/nodes/product/ProductNode.ts
- packages/kg-default-nodes/src/utils/clean-dom.ts
- packages/kg-default-nodes/src/utils/rgb-to-hex.ts
- packages/kg-default-nodes/src/utils/read-caption-from-element.ts
- packages/kg-default-nodes/src/KoenigDecoratorNode.ts
- packages/kg-default-nodes/test/test-utils/should-assertions.d.ts
- packages/kg-default-nodes/src/utils/is-local-content-image.ts
- packages/kg-default-nodes/src/utils/get-resized-image-dimensions.ts
- packages/kg-default-nodes/src/utils/add-create-document-option.ts
- packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-renderer.ts
- packages/kg-default-nodes/test/utils/rgb-to-hex.test.ts
- packages/kg-default-nodes/src/nodes/aside/AsideParser.ts
- packages/kg-default-nodes/src/nodes/button/button-parser.ts
- packages/kg-default-nodes/test/test-utils/overrides.ts
- packages/kg-default-nodes/src/nodes/header/HeaderNode.ts
- packages/kg-default-nodes/src/utils/size-byte-converter.ts
- packages/kg-default-nodes/src/utils/tagged-template-fns.ts
- packages/kg-default-nodes/src/nodes/email-cta/email-cta-renderer.ts
- packages/kg-default-nodes/src/nodes/file/file-renderer.ts
- packages/kg-default-nodes/test/nodes/zwnj.test.ts
- packages/kg-default-nodes/src/nodes/file/file-parser.ts
- packages/kg-default-nodes/test/test-utils/html-minifier.d.ts
- packages/kg-default-nodes/test/test-utils/should.d.ts
- packages/kg-default-nodes/src/nodes/button/button-renderer.ts
- packages/kg-default-nodes/src/nodes/email/EmailNode.ts
- packages/kg-default-nodes/test/nodes/button.test.ts
- packages/kg-default-nodes/test/nodes/toggle.test.ts
- packages/kg-default-nodes/src/utils/read-text-content.ts
- packages/kg-default-nodes/src/nodes/aside/AsideNode.ts
- packages/kg-default-nodes/src/nodes/image/ImageNode.ts
- packages/kg-default-nodes/src/nodes/video/video-parser.ts
- packages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.ts
- packages/kg-default-nodes/test/nodes/signup.test.ts
- packages/kg-default-nodes/src/nodes/bookmark/bookmark-renderer.ts
- packages/kg-default-nodes/src/nodes/audio/audio-parser.ts
- packages/kg-default-nodes/src/nodes/transistor/transistor-renderer.ts
- packages/kg-default-nodes/test/nodes/at-link-search.test.ts
- packages/kg-default-nodes/src/nodes/ExtendedQuoteNode.ts
- packages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.ts
- packages/kg-default-nodes/test/nodes/audio.test.ts
- packages/kg-default-nodes/src/utils/replacement-strings.ts
- packages/kg-default-nodes/src/nodes/email/email-renderer.ts
- packages/kg-default-nodes/src/nodes/html/html-renderer.ts
- packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts
- packages/kg-default-nodes/test/serializers/paragraph.test.ts
- packages/kg-default-nodes/src/utils/read-image-attributes-from-element.ts
- packages/kg-default-nodes/src/nodes/paywall/paywall-renderer.ts
- packages/kg-default-nodes/test/nodes/aside.test.ts
- packages/kg-default-nodes/src/nodes/button/ButtonNode.ts
- packages/kg-default-nodes/test/nodes/transistor.test.ts
- packages/kg-default-nodes/src/nodes/signup/signup-parser.ts
- packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts
- packages/kg-default-nodes/src/utils/srcset-attribute.ts
- packages/kg-default-nodes/src/nodes/image/image-parser.ts
- packages/kg-default-nodes/src/nodes/audio/AudioNode.ts
- packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts
- packages/kg-default-nodes/src/nodes/file/FileNode.ts
- packages/kg-default-nodes/test/nodes/markdown.test.ts
- packages/kg-default-nodes/src/nodes/html/HtmlNode.ts
- packages/kg-default-nodes/test/nodes/video.test.ts
- packages/kg-default-nodes/test/nodes/tk.test.ts
- packages/kg-default-nodes/src/nodes/toggle/ToggleNode.ts
- packages/kg-default-nodes/src/nodes/markdown/MarkdownNode.ts
- packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts
- packages/kg-default-nodes/src/nodes/TKNode.ts
- packages/kg-default-nodes/src/nodes/codeblock/codeblock-parser.ts
- packages/kg-default-nodes/test/nodes/at-link.test.ts
- packages/kg-default-nodes/src/nodes/product/product-parser.ts
- packages/kg-default-nodes/test/nodes/email-cta.test.ts
- packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts
- packages/kg-default-nodes/test/nodes/product.test.ts
- packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts
- packages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.ts
- packages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.ts
- packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts
- packages/kg-default-nodes/src/nodes/header/parsers/header-parser.ts
- packages/kg-default-nodes/src/nodes/image/image-renderer.ts
- packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts
- packages/kg-default-nodes/test/nodes/file.test.ts
- packages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.ts
- packages/kg-default-nodes/src/nodes/transistor/TransistorNode.ts
- packages/kg-default-nodes/test/nodes/embed.test.ts
- packages/kg-default-nodes/test/nodes/image.test.ts
- packages/kg-default-nodes/test/nodes/email.test.ts
- packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts
- packages/kg-default-nodes/src/nodes/signup/signup-renderer.ts
- packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts
- packages/kg-default-nodes/test/nodes/callout.test.ts
- packages/kg-default-nodes/test/nodes/bookmark.test.ts
- packages/kg-default-nodes/src/kg-default-nodes.ts
25f1493 to
c1fdfd3
Compare
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/kg-default-nodes/test/nodes/signup.test.ts (1)
688-695:⚠️ Potential issue | 🟡 MinorBug: Wrong node type tested in
getTextContenttest.The test creates a
PaywallNodevia$createPaywallNode({})but the comment says "signup nodes don't have text content" and this is within theSignupNodetest suite. This should likely be testingSignupNode:🐛 Proposed fix
describe('getTextContent', function () { it('returns contents', editorTest(function () { - const node = $createPaywallNode({}); + const node = $createSignupNode(dataset); // signup nodes don't have text content node.getTextContent().should.equal(''); })); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/signup.test.ts` around lines 688 - 695, The test in the SignupNode suite is creating the wrong node type: replace the call to $createPaywallNode({}) with the Signup node factory ($createSignupNode({})) so the getTextContent test actually instantiates a SignupNode (refer to symbols $createPaywallNode and $createSignupNode and the getTextContent test block) and keep the existing assertion that signup nodes return an empty string.packages/kg-default-nodes/src/nodes/product/product-parser.ts (1)
22-31:⚠️ Potential issue | 🟠 MajorParse image dimensions to numbers in the parser.
The
getAttribute('width')andgetAttribute('height')methods returnstring | null, but theProductNodeinterface declaresproductImageWidthandproductImageHeightasnumber | null. With TypeScript strict mode enabled, this type mismatch will cause compilation errors when the payload is passed to theProductNodeconstructor.Use
parseInt()to convert the string values to numbers before assignment:Fix: Parse to numbers in the parser
if (img.getAttribute('width')) { - payload.productImageWidth = img.getAttribute('width'); + payload.productImageWidth = parseInt(img.getAttribute('width')!, 10); } if (img.getAttribute('height')) { - payload.productImageHeight = img.getAttribute('height'); + payload.productImageHeight = parseInt(img.getAttribute('height')!, 10); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/product/product-parser.ts` around lines 22 - 31, The parser in product-parser.ts assigns img.getAttribute('width'/'height') (string|null) directly to payload.productImageWidth/productImageHeight which are typed as number|null; update the assignment to parse the attribute strings to numbers (e.g., use parseInt or Number) and handle null/NaN by setting the fields to null when no valid numeric value exists so the payload matches the ProductNode types (locate assignments where img.getAttribute(...) is read and replace with parsed-and-validated numeric values).
♻️ Duplicate comments (4)
packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts (2)
145-148:⚠️ Potential issue | 🟠 MajorGuard
contentImageSizesbefore retina-src computation.At Line 147,
getAvailableImageWidthscan throw whenoptions.imageOptimization?.contentImageSizesis absent.🔧 Suggested fix
- if (isLocalContentImage(image.src, options.siteUrl) && options.canTransformImage && options.canTransformImage(image.src)) { + const contentImageSizes = options.imageOptimization?.contentImageSizes; + if (contentImageSizes && isLocalContentImage(image.src, options.siteUrl) && options.canTransformImage?.(image.src)) { // find available image size next up from 2x600 so we can use it for the "retina" src - const availableImageWidths = getAvailableImageWidths(image, options.imageOptimization!.contentImageSizes!); + const availableImageWidths = getAvailableImageWidths(image, contentImageSizes); const srcWidth = availableImageWidths.find(width => width >= 1200);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts` around lines 145 - 148, The code calls getAvailableImageWidths(image, options.imageOptimization!.contentImageSizes!) without guarding that options.imageOptimization and its contentImageSizes exist, which can throw; update the condition around the retina-src computation (the block using isLocalContentImage(...) and calculating srcWidth) to first check options.imageOptimization && options.imageOptimization.contentImageSizes (or use optional chaining) before calling getAvailableImageWidths, and if absent either skip the retina-src logic or use a safe fallback so getAvailableImageWidths is never invoked with undefined; refer to isLocalContentImage, getAvailableImageWidths, options.imageOptimization.contentImageSizes and the srcWidth usage when making the change.
1-8:⚠️ Potential issue | 🔴 CriticalSanitize
captionbefore assigning toinnerHTML.Line 181 inserts
node.captiondirectly as HTML, which can enable scriptable markup injection.🔧 Suggested fix
import {renderEmptyContainer} from '../../utils/render-empty-container.js'; +import {buildCleanBasicHtmlForElement} from '../../utils/build-clean-basic-html-for-element.js'; @@ if (node.caption) { const figcaption = document.createElement('figcaption'); - figcaption.innerHTML = node.caption; + const cleanBasicHtml = buildCleanBasicHtmlForElement(figcaption); + const cleanedCaption = cleanBasicHtml(node.caption); + figcaption.innerHTML = cleanedCaption || ''; figure.appendChild(figcaption); figure.setAttribute('class', `${figure.getAttribute('class')} kg-card-hascaption`); }Also applies to: 179-182
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts` around lines 1 - 8, The caption string (node.caption) is being inserted directly into element.innerHTML (around the gallery renderer logic), which allows scriptable markup injection; change the code that assigns node.caption to the DOM so it either sets element.textContent / uses createTextNode or passes node.caption through a trusted HTML sanitizer (e.g., DOMPurify.sanitize) before assigning to innerHTML; implement a small helper (e.g., sanitizeCaption) and use it where node.caption is currently used so all caption insertions are sanitized consistently.packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts (1)
401-407:⚠️ Potential issue | 🔴 CriticalSanitize
sponsorLabelbefore the email early-return path.At Line 401, the email branch returns before the sanitizer at Line 412, so Line 405 can render raw
dataset.sponsorLabel.🔧 Suggested fix
export function renderCallToActionNode(node: CTANodeData, options: CTARenderOptions = {}) { addCreateDocumentOption(options); const document = options.createDocument!(); @@ if (!dataset.backgroundColor || !dataset.backgroundColor.match(/^[a-zA-Z\d-]+|#([a-fA-F\d]{3}|[a-fA-F\d]{6})$/)) { dataset.backgroundColor = 'white'; } + if (dataset.hasSponsorLabel) { + const cleanBasicHtml = buildCleanBasicHtmlForElement(document.createElement('div')); + const cleanedHtml = cleanBasicHtml(dataset.sponsorLabel, {firstChildInnerContent: true}); + dataset.sponsorLabel = cleanedHtml || ''; + } + if (options.target === 'email') { const emailDoc = options.createDocument!(); const emailDiv = emailDoc.createElement('div'); @@ - if (dataset.hasSponsorLabel) { - const cleanBasicHtml = buildCleanBasicHtmlForElement(element); - const cleanedHtml = cleanBasicHtml(dataset.sponsorLabel, {firstChildInnerContent: true}); - dataset.sponsorLabel = cleanedHtml || ''; - } const htmlString = ctaCardTemplate(dataset);Also applies to: 412-416
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts` around lines 401 - 407, The email branch returns before the sponsor label sanitizer runs, so ensure dataset.sponsorLabel is sanitized before you build the email HTML and return: call the existing sanitizer (the one currently used later in this file) to produce a safeSponsorLabel and use that when calling emailCTATemplate(datasetWithSafeSponsor, options) or otherwise substitute dataset.sponsorLabel with the sanitized value; do the same fix for the other branch that uses raw sponsorLabel (the block around renderWithVisibility/DOM rendering) so all paths use the sanitized sponsor label instead of the raw dataset.sponsorLabel.packages/kg-default-nodes/package.json (1)
19-21:⚠️ Potential issue | 🟠 MajorStill unresolved: clean
build/before recompiling.
tscwill not remove outputs for renamed or deleted sources. Because this package now publishes the wholebuild/tree, stale JS or.d.tsfiles can survive a local rebuild and get packed.🧹 Suggested script update
"scripts": { "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", + "clean": "node -e \"require('node:fs').rmSync('build', {recursive: true, force: true})\"", + "build": "yarn clean && tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"module\"}' > build/esm/package.json", + "prepare": "yarn build", + "pretest": "yarn build && tsc -p tsconfig.test.json",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/package.json` around lines 19 - 21, The build scripts ("build", "prepare", "pretest") leave stale artifacts because tsc doesn't delete removed/renamed outputs; update each script to remove the existing build/ directory before running the TypeScript compiles (e.g., call a "clean" step or invoke rimraf/rm -rf build) so the sequence becomes clean then tsc && tsc -p tsconfig.cjs.json ...; add a "clean" script if using rimraf and ensure "build", "prepare", and "pretest" all invoke that clean step first to guarantee a fresh build tree.
🧹 Nitpick comments (10)
packages/kg-default-nodes/src/nodes/aside/AsideParser.ts (1)
4-7: Consider makingNodeClassimmutable.Line 4 can be
readonlysince it is only assigned in the constructor, which tightens class invariants.♻️ Suggested tweak
- NodeClass: {new (): LexicalNode}; + readonly NodeClass: {new (): LexicalNode};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/aside/AsideParser.ts` around lines 4 - 7, Make the NodeClass property immutable: change the class field declaration of NodeClass to be readonly (e.g., readonly NodeClass: { new(): LexicalNode }) since it is only assigned in the constructor; update the class field declaration and keep the assignment in the constructor unchanged (refer to NodeClass and the constructor in AsideParser).packages/kg-default-nodes/test/serializers/linebreak.test.ts (1)
47-50: Type assertions are correct but repetitive.The
as ElementNodecasts are necessary since$generateNodesFromDOMreturnsLexicalNode[], andgetChildren()is only available onElementNode. However, casting the same node multiple times is verbose.Consider extracting to a typed variable for readability:
♻️ Optional: Extract repeated casts to a variable
should.equal(nodes.length, 1); should.equal(nodes[0].getType(), 'paragraph'); - should.equal((nodes[0] as ElementNode).getChildren().length, 3); - should.equal((nodes[0] as ElementNode).getChildren()[0].getType(), 'extended-text'); - should.equal((nodes[0] as ElementNode).getChildren()[1].getType(), 'linebreak'); - should.equal((nodes[0] as ElementNode).getChildren()[2].getType(), 'extended-text'); + const paragraph = nodes[0] as ElementNode; + should.equal(paragraph.getChildren().length, 3); + should.equal(paragraph.getChildren()[0].getType(), 'extended-text'); + should.equal(paragraph.getChildren()[1].getType(), 'linebreak'); + should.equal(paragraph.getChildren()[2].getType(), 'extended-text');This same pattern applies to other test cases (lines 63-65, 70-72, 82-84, 87-89) where the cast is repeated.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/serializers/linebreak.test.ts` around lines 47 - 50, The repeated casts like (nodes[0] as ElementNode) are verbose; replace each group of repeated casts by extracting a typed variable (e.g., const element = nodes[0] as ElementNode) and use element.getChildren() and element.getChildren()[i].getType() in the assertions; do the same refactor for the other test blocks that repeat the cast (the subsequent assertions referencing getChildren()), keeping the same assertion logic but using the single typed variable to improve readability.packages/kg-default-nodes/test/nodes/email.test.ts (1)
158-159: Consider defining a dedicated type for export options.The
as unknown as LexicalEditordouble-cast pattern is used repeatedly to passexportOptionstoexportDOM(). While this works at runtime and is consistent with similar test files in this PR, it completely bypasses type safety.If this pattern is prevalent across many test files, consider defining a shared type (e.g.,
ExportDOMOptions) that accurately describes the options object, and updating theexportDOMmethod signature or creating a test helper to avoid the double-cast.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/email.test.ts` around lines 158 - 159, The test uses a double-cast (as unknown as LexicalEditor) when calling emailNode.exportDOM({...exportOptions, ...options}) which bypasses type safety; create a shared type (e.g., ExportDOMOptions) that models the options passed to exportDOM and update the test to cast to that type (or add a small test helper that returns a properly typed object) and, if appropriate, adjust the exportDOM signature to accept ExportDOMOptions instead of forcing a LexicalEditor cast; update references to exportOptions, emailNode.exportDOM, and any other tests using this pattern to use the new ExportDOMOptions or helper so the double-cast is removed.packages/kg-default-nodes/src/nodes/transistor/transistor-renderer.ts (1)
47-47: Consider adding a brief comment explaining the double cast.The
as unknown aspattern is used twice here to bridge DOM type differences between the standardDocumentand theBrowserDocumenttype expected bybuildSrcBackgroundScript. While necessary for this migration, a brief inline comment would help future maintainers understand why the workaround exists.💡 Suggested comment
- figure.insertAdjacentElement('beforeend', buildSrcBackgroundScript(document as unknown as Parameters<typeof buildSrcBackgroundScript>[0]) as unknown as Element); + // Cast needed: document type differs from BrowserDocument expected by buildSrcBackgroundScript + figure.insertAdjacentElement('beforeend', buildSrcBackgroundScript(document as unknown as Parameters<typeof buildSrcBackgroundScript>[0]) as unknown as Element);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/transistor/transistor-renderer.ts` at line 47, The double cast using "as unknown as" when calling buildSrcBackgroundScript(document ...) is a necessary DOM-type workaround but lacks explanation; add a succinct inline comment next to the call that explains why Document is being cast to the BrowserDocument type expected by buildSrcBackgroundScript (e.g., to bridge differing DOM typings during migration) and reference the involved symbols (buildSrcBackgroundScript and the insertion into figure via insertAdjacentElement) so future maintainers understand the type workaround.packages/kg-default-nodes/src/nodes/embed/embed-parser.ts (1)
90-92: Mutating the original DOM element'ssrcmay cause side effects.Assigning to
iframe.srcmodifies the passed DOM element in place. If the parser is called during live DOM processing (e.g., drag-and-drop or copy-paste), this could unexpectedly alter the original element.Consider working with the value instead:
♻️ Suggested refactor
- // if it's a schemaless URL, convert to https - if (iframe.src.match(/^\/\//)) { - iframe.src = `https:${iframe.src}`; - } + // if it's a schemaless URL, convert to https + let src = iframe.src; + if (src.match(/^\/\//)) { + src = `https:${src}`; + } const payload: Record<string, unknown> = { - url: iframe.src + url: src };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/embed/embed-parser.ts` around lines 90 - 92, The code mutates the passed DOM element by assigning to iframe.src; instead, read and normalize the value into a local variable (e.g., const src = iframe.src or let normalizedSrc = iframe.src), if it matches /^\/\// prefix prepend "https:" to that local variable, and use the normalizedSrc for further parsing/return values without writing back to iframe.src; update the logic in embed-parser.ts (the block handling iframe and iframe.src) to avoid modifying the original iframe element.packages/kg-default-nodes/src/nodes/toggle/toggle-renderer.ts (1)
9-15: Consolidate duplicatedRenderOptionsinto a shared type.This
RenderOptionsshape is repeated in multiple renderers (for example,packages/kg-default-nodes/src/nodes/button/button-renderer.tsLines 12-18). Centralizing it would reduce drift and maintenance overhead.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/toggle/toggle-renderer.ts` around lines 9 - 15, The RenderOptions interface declared in toggle-renderer.ts is duplicated across renderers (e.g., button-renderer.ts); extract it to a single shared exported type (e.g., export type RenderOptions = { createDocument?: () => Document; dom?: { window: { document: Document } }; target?: string; feature?: { emailCustomization?: boolean; emailCustomizationAlpha?: boolean }; [key: string]: unknown }) in a common module (shared types file) and update toggle-renderer.ts and other renderers to import and use this shared RenderOptions type instead of declaring it inline (ensure the symbol name RenderOptions is used where referenced).packages/kg-default-nodes/test/nodes/tk.test.ts (2)
109-115: Same pattern as noted above.The cast
as TKNodeon line 111 follows the same pattern as lines 36/41 and could similarly be removed if the return type of$createTKNodeis correctly inferred asTKNode.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/tk.test.ts` around lines 109 - 115, Remove the unnecessary cast on the test's $createTKNode call: instead of casting the result to TKNode when calling TKNode.clone, make $createTKNode's return type correctly inferred as TKNode (or update its type signature) so the cast can be dropped; locate uses in this test (the $createTKNode call and TKNode.clone invocation) and remove the "as TKNode" cast, or adjust the $createTKNode declaration so its return type is TKNode to avoid needing casts.
34-42: Remove redundant type casts.The
$createTKNodefunction returnsTKNode(inferred from$applyNodeReplacement(new TKNode(text))), so the casts on lines 36 and 41 are unnecessary. This is evident from line 47, wheretkNode.exportJSON()is called without any cast.♻️ Remove redundant casts
it('is a text entity', editorTest(function () { const tkNode = $createTKNode('TK'); - (tkNode as TKNode).isTextEntity().should.be.true(); + tkNode.isTextEntity().should.be.true(); })); it('can not insert text before', editorTest(function () { const tkNode = $createTKNode('TK'); - (tkNode as TKNode).canInsertTextBefore().should.be.false(); + tkNode.canInsertTextBefore().should.be.false(); }));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/tk.test.ts` around lines 34 - 42, Remove the redundant casts to TKNode: $createTKNode already returns a TKNode so drop the (tkNode as TKNode) casts in the tests where isTextEntity() and canInsertTextBefore() are called; change calls to tkNode.isTextEntity() and tkNode.canInsertTextBefore() (leave other usages like tkNode.exportJSON() untouched).packages/kg-default-nodes/src/generate-decorator-node.ts (2)
32-52: JSDoc is missing theurlPathproperty.The
DecoratorNodePropertyinterface includesurlPath?: string(line 49), but the JSDoc above doesn't document this property. Consider updating the JSDoc for completeness.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/generate-decorator-node.ts` around lines 32 - 52, The JSDoc for DecoratorNodeProperty is missing the urlPath property; update the comment block above the interface to add an `@property` for urlPath (e.g., `@property` {string} [urlPath] - optional path segment used when the property contains a URL or to resolve relative URLs) so the documented properties match the DecoratorNodeProperty interface (which currently declares urlPath?: string).
222-233: Acceptable use of@ts-expect-errorfor migration.The comment indicates this is part of the strict mode migration. The return type
Record<string, unknown>withtypeandversionproperties may not perfectly match the parent class signature. Consider documenting what the expected mismatch is or planning to resolve this post-migration.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/generate-decorator-node.ts` around lines 222 - 233, The exportJSON method uses `@ts-expect-error` because its declared return Record<string, unknown> (built from nodeType, version and internalProps) doesn't match the parent signature; update exportJSON in the class to return the correct type or add an explicit, narrow cast to the parent return type to remove the error (referencing exportJSON, nodeType, version, and internalProps), or add a short inline comment explaining the specific shape mismatch and plan to fix the parent type post-migration so the `@ts-expect-error` can be removed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/kg-default-nodes/package.json`:
- Line 18: The "dev" script currently only runs "tsc --watch
--preserveWatchOutput" which performs a simple TS transpile and misses emitting
the published layout (build/cjs/index.js and build/esm/package.json); update the
"dev" script entry named "dev" in package.json to run the TypeScript build mode
in watch (e.g. use "tsc --build --watch --preserveWatchOutput" or "tsc -b
--watch --preserveWatchOutput") so project references and the packaging steps
run on start and produce the correct build/cjs and build/esm outputs immediately
after a clean checkout.
In `@packages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.ts`:
- Around line 40-41: The figcaption assignment currently uses
figcaption.innerHTML = node.caption which is an XSS sink; change it to set plain
text instead (e.g., use figcaption.textContent or document.createTextNode) so
captions are rendered as text rather than raw HTML, and if HTML captions must be
supported ensure they are explicitly sanitized before assignment; update the
code in codeblock-renderer.ts where figcaption is created and assigned (look for
figcaption.innerHTML and replace with a safe text assignment or sanitized HTML).
In `@packages/kg-default-nodes/src/nodes/embed/types/twitter.ts`:
- Around line 79-95: Several optional Tweet fields are being dereferenced
directly; guard them first to avoid runtime errors by using null/undefined
checks or optional chaining and sensible fallbacks: read
tweetData.public_metrics via tweetData.public_metrics?.retweet_count and
tweetData.public_metrics?.like_count with defaults before calling
numberFormatter (affecting retweetCount and likeCount), find authorUser only if
tweetData.users and tweetData.author_id exist, call DateTime.fromISO for
tweetTime/tweetDate only when tweetData.created_at is present, derive
mentions/urls/hashtags from tweetData.entities with default empty arrays as
currently done, and compute tweetImageUrl only after verifying
tweetData.attachments/media and tweetData.includes?.media[0] exist (affecting
hasImageOrVideo and tweetImageUrl); update those expressions in this file
(variables: retweetCount, likeCount, authorUser, tweetTime, tweetDate,
hasImageOrVideo, tweetImageUrl) to use optional chaining/null checks and
defaults.
In `@packages/kg-default-nodes/src/nodes/file/file-parser.ts`:
- Line 16: The value assigned to fileSize in file-parser.ts is a number from
sizeToBytes but FileNode.fileSize is typed as string and the formattedFileSize
getter expects a string; change the assignment used when constructing the
FileNode (the const fileSize local and/or the value passed into the FileNode) to
String(sizeToBytes(...)) so FileNode.fileSize receives a string (refer to the
fileSize const and the FileNode/interface and formattedFileSize getter to locate
the assignment).
In `@packages/kg-default-nodes/src/nodes/product/product-renderer.ts`:
- Around line 96-104: The code assumes data.productImageWidth and
data.productImageHeight are numbers by using "as number", which can hide string
values from the parser and cause runtime bugs; update the product renderer logic
(references: productImageWidth, productImageHeight, imageDimensions,
getResizedImageDimensions) to defensively coerce and validate these values
(e.g., Number(...) or parseInt/parseFloat) before using them, check for NaN or
non-finite results and skip/handle assignment if invalid, and use the validated
numeric value when comparing to 560 and when calling getResizedImageDimensions
so comparisons and resizing operate on real numbers.
- Around line 56-61: The template interpolation in product-renderer.ts currently
injects unescaped user fields (productTitle, productDescription, productUrl,
productButton) into htmlString before assigning to element.innerHTML; import
escapeHtml from ../../utils/escape-html.js at the top and wrap every
user-supplied value passed into the cardTemplate and email template with
escapeHtml (use escaped productTitle, productDescription, productButton for text
nodes and escapeHtml(productUrl) for href attributes) so the returned element
(from the function that builds htmlString and returns {element:
element.firstElementChild}) receives safe, escaped content consistent with
file-renderer.ts.
In `@packages/kg-default-nodes/src/utils/tagged-template-fns.ts`:
- Around line 8-10: The reduce callback building `result` currently uses
(values[i] || '') which treats valid falsy values like 0 and false as empty;
change that fallback to nullish coalescing so only null/undefined become '' —
i.e., update the reducer expression in the strings.reduce callback (the code
that computes `result`) to use (values[i] ?? '') instead of (values[i] || '').
In `@packages/kg-default-nodes/test/nodes/toggle.test.ts`:
- Line 168: The exportDOM calls on toggleNode are incorrectly casting
exportOptions to LexicalEditor (e.g., toggleNode.exportDOM(exportOptions as
unknown as LexicalEditor)); remove the unnecessary and unsafe "as unknown as
LexicalEditor" casts and call exportDOM with the options object directly
(toggleNode.exportDOM(exportOptions)), keeping the final type assertion to
{element: HTMLElement} as needed; update all occurrences where exportDOM is
called with that double-cast (the calls using exportOptions) to use the direct
call instead.
---
Outside diff comments:
In `@packages/kg-default-nodes/src/nodes/product/product-parser.ts`:
- Around line 22-31: The parser in product-parser.ts assigns
img.getAttribute('width'/'height') (string|null) directly to
payload.productImageWidth/productImageHeight which are typed as number|null;
update the assignment to parse the attribute strings to numbers (e.g., use
parseInt or Number) and handle null/NaN by setting the fields to null when no
valid numeric value exists so the payload matches the ProductNode types (locate
assignments where img.getAttribute(...) is read and replace with
parsed-and-validated numeric values).
In `@packages/kg-default-nodes/test/nodes/signup.test.ts`:
- Around line 688-695: The test in the SignupNode suite is creating the wrong
node type: replace the call to $createPaywallNode({}) with the Signup node
factory ($createSignupNode({})) so the getTextContent test actually instantiates
a SignupNode (refer to symbols $createPaywallNode and $createSignupNode and the
getTextContent test block) and keep the existing assertion that signup nodes
return an empty string.
---
Duplicate comments:
In `@packages/kg-default-nodes/package.json`:
- Around line 19-21: The build scripts ("build", "prepare", "pretest") leave
stale artifacts because tsc doesn't delete removed/renamed outputs; update each
script to remove the existing build/ directory before running the TypeScript
compiles (e.g., call a "clean" step or invoke rimraf/rm -rf build) so the
sequence becomes clean then tsc && tsc -p tsconfig.cjs.json ...; add a "clean"
script if using rimraf and ensure "build", "prepare", and "pretest" all invoke
that clean step first to guarantee a fresh build tree.
In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts`:
- Around line 401-407: The email branch returns before the sponsor label
sanitizer runs, so ensure dataset.sponsorLabel is sanitized before you build the
email HTML and return: call the existing sanitizer (the one currently used later
in this file) to produce a safeSponsorLabel and use that when calling
emailCTATemplate(datasetWithSafeSponsor, options) or otherwise substitute
dataset.sponsorLabel with the sanitized value; do the same fix for the other
branch that uses raw sponsorLabel (the block around renderWithVisibility/DOM
rendering) so all paths use the sanitized sponsor label instead of the raw
dataset.sponsorLabel.
In `@packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts`:
- Around line 145-148: The code calls getAvailableImageWidths(image,
options.imageOptimization!.contentImageSizes!) without guarding that
options.imageOptimization and its contentImageSizes exist, which can throw;
update the condition around the retina-src computation (the block using
isLocalContentImage(...) and calculating srcWidth) to first check
options.imageOptimization && options.imageOptimization.contentImageSizes (or use
optional chaining) before calling getAvailableImageWidths, and if absent either
skip the retina-src logic or use a safe fallback so getAvailableImageWidths is
never invoked with undefined; refer to isLocalContentImage,
getAvailableImageWidths, options.imageOptimization.contentImageSizes and the
srcWidth usage when making the change.
- Around line 1-8: The caption string (node.caption) is being inserted directly
into element.innerHTML (around the gallery renderer logic), which allows
scriptable markup injection; change the code that assigns node.caption to the
DOM so it either sets element.textContent / uses createTextNode or passes
node.caption through a trusted HTML sanitizer (e.g., DOMPurify.sanitize) before
assigning to innerHTML; implement a small helper (e.g., sanitizeCaption) and use
it where node.caption is currently used so all caption insertions are sanitized
consistently.
---
Nitpick comments:
In `@packages/kg-default-nodes/src/generate-decorator-node.ts`:
- Around line 32-52: The JSDoc for DecoratorNodeProperty is missing the urlPath
property; update the comment block above the interface to add an `@property` for
urlPath (e.g., `@property` {string} [urlPath] - optional path segment used when
the property contains a URL or to resolve relative URLs) so the documented
properties match the DecoratorNodeProperty interface (which currently declares
urlPath?: string).
- Around line 222-233: The exportJSON method uses `@ts-expect-error` because its
declared return Record<string, unknown> (built from nodeType, version and
internalProps) doesn't match the parent signature; update exportJSON in the
class to return the correct type or add an explicit, narrow cast to the parent
return type to remove the error (referencing exportJSON, nodeType, version, and
internalProps), or add a short inline comment explaining the specific shape
mismatch and plan to fix the parent type post-migration so the `@ts-expect-error`
can be removed.
In `@packages/kg-default-nodes/src/nodes/aside/AsideParser.ts`:
- Around line 4-7: Make the NodeClass property immutable: change the class field
declaration of NodeClass to be readonly (e.g., readonly NodeClass: { new():
LexicalNode }) since it is only assigned in the constructor; update the class
field declaration and keep the assignment in the constructor unchanged (refer to
NodeClass and the constructor in AsideParser).
In `@packages/kg-default-nodes/src/nodes/embed/embed-parser.ts`:
- Around line 90-92: The code mutates the passed DOM element by assigning to
iframe.src; instead, read and normalize the value into a local variable (e.g.,
const src = iframe.src or let normalizedSrc = iframe.src), if it matches /^\/\//
prefix prepend "https:" to that local variable, and use the normalizedSrc for
further parsing/return values without writing back to iframe.src; update the
logic in embed-parser.ts (the block handling iframe and iframe.src) to avoid
modifying the original iframe element.
In `@packages/kg-default-nodes/src/nodes/toggle/toggle-renderer.ts`:
- Around line 9-15: The RenderOptions interface declared in toggle-renderer.ts
is duplicated across renderers (e.g., button-renderer.ts); extract it to a
single shared exported type (e.g., export type RenderOptions = {
createDocument?: () => Document; dom?: { window: { document: Document } };
target?: string; feature?: { emailCustomization?: boolean;
emailCustomizationAlpha?: boolean }; [key: string]: unknown }) in a common
module (shared types file) and update toggle-renderer.ts and other renderers to
import and use this shared RenderOptions type instead of declaring it inline
(ensure the symbol name RenderOptions is used where referenced).
In `@packages/kg-default-nodes/src/nodes/transistor/transistor-renderer.ts`:
- Line 47: The double cast using "as unknown as" when calling
buildSrcBackgroundScript(document ...) is a necessary DOM-type workaround but
lacks explanation; add a succinct inline comment next to the call that explains
why Document is being cast to the BrowserDocument type expected by
buildSrcBackgroundScript (e.g., to bridge differing DOM typings during
migration) and reference the involved symbols (buildSrcBackgroundScript and the
insertion into figure via insertAdjacentElement) so future maintainers
understand the type workaround.
In `@packages/kg-default-nodes/test/nodes/email.test.ts`:
- Around line 158-159: The test uses a double-cast (as unknown as LexicalEditor)
when calling emailNode.exportDOM({...exportOptions, ...options}) which bypasses
type safety; create a shared type (e.g., ExportDOMOptions) that models the
options passed to exportDOM and update the test to cast to that type (or add a
small test helper that returns a properly typed object) and, if appropriate,
adjust the exportDOM signature to accept ExportDOMOptions instead of forcing a
LexicalEditor cast; update references to exportOptions, emailNode.exportDOM, and
any other tests using this pattern to use the new ExportDOMOptions or helper so
the double-cast is removed.
In `@packages/kg-default-nodes/test/nodes/tk.test.ts`:
- Around line 109-115: Remove the unnecessary cast on the test's $createTKNode
call: instead of casting the result to TKNode when calling TKNode.clone, make
$createTKNode's return type correctly inferred as TKNode (or update its type
signature) so the cast can be dropped; locate uses in this test (the
$createTKNode call and TKNode.clone invocation) and remove the "as TKNode" cast,
or adjust the $createTKNode declaration so its return type is TKNode to avoid
needing casts.
- Around line 34-42: Remove the redundant casts to TKNode: $createTKNode already
returns a TKNode so drop the (tkNode as TKNode) casts in the tests where
isTextEntity() and canInsertTextBefore() are called; change calls to
tkNode.isTextEntity() and tkNode.canInsertTextBefore() (leave other usages like
tkNode.exportJSON() untouched).
In `@packages/kg-default-nodes/test/serializers/linebreak.test.ts`:
- Around line 47-50: The repeated casts like (nodes[0] as ElementNode) are
verbose; replace each group of repeated casts by extracting a typed variable
(e.g., const element = nodes[0] as ElementNode) and use element.getChildren()
and element.getChildren()[i].getType() in the assertions; do the same refactor
for the other test blocks that repeat the cast (the subsequent assertions
referencing getChildren()), keeping the same assertion logic but using the
single typed variable to improve readability.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3c072553-4f9f-416a-bd25-5546754776be
⛔ Files ignored due to path filters (1)
packages/kg-default-nodes/src/nodes/at-link/kg-link.svgis excluded by!**/*.svg
📒 Files selected for processing (161)
packages/kg-default-nodes/eslint.config.mjspackages/kg-default-nodes/index.jspackages/kg-default-nodes/lib/kg-default-nodes.jspackages/kg-default-nodes/lib/nodes/at-link/index.jspackages/kg-default-nodes/lib/nodes/codeblock/codeblock-parser.jspackages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-parser.jspackages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-renderer.jspackages/kg-default-nodes/lib/nodes/html/html-parser.jspackages/kg-default-nodes/lib/nodes/markdown/markdown-renderer.jspackages/kg-default-nodes/lib/nodes/paywall/paywall-parser.jspackages/kg-default-nodes/lib/utils/is-unsplash-image.jspackages/kg-default-nodes/package.jsonpackages/kg-default-nodes/rollup.config.mjspackages/kg-default-nodes/src/KoenigDecoratorNode.tspackages/kg-default-nodes/src/generate-decorator-node.tspackages/kg-default-nodes/src/index.tspackages/kg-default-nodes/src/kg-default-nodes.tspackages/kg-default-nodes/src/nodes/ExtendedHeadingNode.tspackages/kg-default-nodes/src/nodes/ExtendedQuoteNode.tspackages/kg-default-nodes/src/nodes/ExtendedTextNode.tspackages/kg-default-nodes/src/nodes/TKNode.tspackages/kg-default-nodes/src/nodes/aside/AsideNode.tspackages/kg-default-nodes/src/nodes/aside/AsideParser.tspackages/kg-default-nodes/src/nodes/at-link/AtLinkNode.tspackages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.tspackages/kg-default-nodes/src/nodes/at-link/index.tspackages/kg-default-nodes/src/nodes/audio/AudioNode.tspackages/kg-default-nodes/src/nodes/audio/audio-parser.tspackages/kg-default-nodes/src/nodes/audio/audio-renderer.tspackages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.tspackages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.tspackages/kg-default-nodes/src/nodes/bookmark/bookmark-renderer.tspackages/kg-default-nodes/src/nodes/button/ButtonNode.tspackages/kg-default-nodes/src/nodes/button/button-parser.tspackages/kg-default-nodes/src/nodes/button/button-renderer.tspackages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.tspackages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.tspackages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.tspackages/kg-default-nodes/src/nodes/callout/CalloutNode.tspackages/kg-default-nodes/src/nodes/callout/callout-parser.tspackages/kg-default-nodes/src/nodes/callout/callout-renderer.tspackages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.tspackages/kg-default-nodes/src/nodes/codeblock/codeblock-parser.tspackages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.tspackages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.tspackages/kg-default-nodes/src/nodes/email-cta/email-cta-renderer.tspackages/kg-default-nodes/src/nodes/email/EmailNode.tspackages/kg-default-nodes/src/nodes/email/email-renderer.tspackages/kg-default-nodes/src/nodes/embed/EmbedNode.tspackages/kg-default-nodes/src/nodes/embed/embed-parser.tspackages/kg-default-nodes/src/nodes/embed/embed-renderer.tspackages/kg-default-nodes/src/nodes/embed/types/twitter.tspackages/kg-default-nodes/src/nodes/file/FileNode.tspackages/kg-default-nodes/src/nodes/file/file-parser.tspackages/kg-default-nodes/src/nodes/file/file-renderer.tspackages/kg-default-nodes/src/nodes/gallery/GalleryNode.tspackages/kg-default-nodes/src/nodes/gallery/gallery-parser.tspackages/kg-default-nodes/src/nodes/gallery/gallery-renderer.tspackages/kg-default-nodes/src/nodes/header/HeaderNode.tspackages/kg-default-nodes/src/nodes/header/parsers/header-parser.tspackages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.tspackages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.tspackages/kg-default-nodes/src/nodes/horizontalrule/HorizontalRuleNode.tspackages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-parser.tspackages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-renderer.tspackages/kg-default-nodes/src/nodes/html/HtmlNode.tspackages/kg-default-nodes/src/nodes/html/html-parser.tspackages/kg-default-nodes/src/nodes/html/html-renderer.tspackages/kg-default-nodes/src/nodes/image/ImageNode.tspackages/kg-default-nodes/src/nodes/image/image-parser.tspackages/kg-default-nodes/src/nodes/image/image-renderer.tspackages/kg-default-nodes/src/nodes/markdown/MarkdownNode.tspackages/kg-default-nodes/src/nodes/markdown/markdown-renderer.tspackages/kg-default-nodes/src/nodes/paywall/PaywallNode.tspackages/kg-default-nodes/src/nodes/paywall/paywall-parser.tspackages/kg-default-nodes/src/nodes/paywall/paywall-renderer.tspackages/kg-default-nodes/src/nodes/product/ProductNode.tspackages/kg-default-nodes/src/nodes/product/product-parser.tspackages/kg-default-nodes/src/nodes/product/product-renderer.tspackages/kg-default-nodes/src/nodes/signup/SignupNode.tspackages/kg-default-nodes/src/nodes/signup/signup-parser.tspackages/kg-default-nodes/src/nodes/signup/signup-renderer.tspackages/kg-default-nodes/src/nodes/toggle/ToggleNode.tspackages/kg-default-nodes/src/nodes/toggle/toggle-parser.tspackages/kg-default-nodes/src/nodes/toggle/toggle-renderer.tspackages/kg-default-nodes/src/nodes/transistor/TransistorNode.tspackages/kg-default-nodes/src/nodes/transistor/transistor-renderer.tspackages/kg-default-nodes/src/nodes/video/VideoNode.tspackages/kg-default-nodes/src/nodes/video/video-parser.tspackages/kg-default-nodes/src/nodes/video/video-renderer.tspackages/kg-default-nodes/src/nodes/zwnj/ZWNJNode.tspackages/kg-default-nodes/src/serializers/linebreak.tspackages/kg-default-nodes/src/serializers/paragraph.tspackages/kg-default-nodes/src/svg.d.tspackages/kg-default-nodes/src/utils/add-create-document-option.tspackages/kg-default-nodes/src/utils/build-clean-basic-html-for-element.tspackages/kg-default-nodes/src/utils/clean-dom.tspackages/kg-default-nodes/src/utils/escape-html.tspackages/kg-default-nodes/src/utils/get-available-image-widths.tspackages/kg-default-nodes/src/utils/get-resized-image-dimensions.tspackages/kg-default-nodes/src/utils/is-local-content-image.tspackages/kg-default-nodes/src/utils/is-unsplash-image.tspackages/kg-default-nodes/src/utils/read-caption-from-element.tspackages/kg-default-nodes/src/utils/read-image-attributes-from-element.tspackages/kg-default-nodes/src/utils/read-text-content.tspackages/kg-default-nodes/src/utils/render-empty-container.tspackages/kg-default-nodes/src/utils/render-helpers/email-button.tspackages/kg-default-nodes/src/utils/replacement-strings.tspackages/kg-default-nodes/src/utils/rgb-to-hex.tspackages/kg-default-nodes/src/utils/set-src-background-from-parent.tspackages/kg-default-nodes/src/utils/size-byte-converter.tspackages/kg-default-nodes/src/utils/slugify.tspackages/kg-default-nodes/src/utils/srcset-attribute.tspackages/kg-default-nodes/src/utils/tagged-template-fns.tspackages/kg-default-nodes/src/utils/truncate.tspackages/kg-default-nodes/src/utils/visibility.tspackages/kg-default-nodes/test/generate-decorator-node.test.tspackages/kg-default-nodes/test/nodes/aside.test.tspackages/kg-default-nodes/test/nodes/at-link-search.test.tspackages/kg-default-nodes/test/nodes/at-link.test.tspackages/kg-default-nodes/test/nodes/audio.test.tspackages/kg-default-nodes/test/nodes/bookmark.test.tspackages/kg-default-nodes/test/nodes/button.test.tspackages/kg-default-nodes/test/nodes/call-to-action.test.tspackages/kg-default-nodes/test/nodes/callout.test.tspackages/kg-default-nodes/test/nodes/codeblock.test.tspackages/kg-default-nodes/test/nodes/email-cta.test.tspackages/kg-default-nodes/test/nodes/email.test.tspackages/kg-default-nodes/test/nodes/embed.test.tspackages/kg-default-nodes/test/nodes/file.test.tspackages/kg-default-nodes/test/nodes/gallery.test.tspackages/kg-default-nodes/test/nodes/header.test.tspackages/kg-default-nodes/test/nodes/horizontalrule.test.tspackages/kg-default-nodes/test/nodes/html.test.tspackages/kg-default-nodes/test/nodes/image.test.tspackages/kg-default-nodes/test/nodes/markdown.test.tspackages/kg-default-nodes/test/nodes/paywall.test.tspackages/kg-default-nodes/test/nodes/product.test.tspackages/kg-default-nodes/test/nodes/signup.test.tspackages/kg-default-nodes/test/nodes/tk.test.tspackages/kg-default-nodes/test/nodes/toggle.test.tspackages/kg-default-nodes/test/nodes/transistor.test.tspackages/kg-default-nodes/test/nodes/video.test.tspackages/kg-default-nodes/test/nodes/zwnj.test.tspackages/kg-default-nodes/test/serializers/linebreak.test.tspackages/kg-default-nodes/test/serializers/paragraph.test.tspackages/kg-default-nodes/test/test-utils/assertions.jspackages/kg-default-nodes/test/test-utils/assertions.tspackages/kg-default-nodes/test/test-utils/html-minifier.d.tspackages/kg-default-nodes/test/test-utils/index.jspackages/kg-default-nodes/test/test-utils/index.tspackages/kg-default-nodes/test/test-utils/overrides.jspackages/kg-default-nodes/test/test-utils/overrides.tspackages/kg-default-nodes/test/test-utils/should-assertions.d.tspackages/kg-default-nodes/test/test-utils/should.d.tspackages/kg-default-nodes/test/utils/rgb-to-hex.test.tspackages/kg-default-nodes/test/utils/tagged-template-fns.test.tspackages/kg-default-nodes/test/utils/visibility.test.tspackages/kg-default-nodes/tsconfig.cjs.jsonpackages/kg-default-nodes/tsconfig.jsonpackages/kg-default-nodes/tsconfig.test.json
💤 Files with no reviewable changes (14)
- packages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-renderer.js
- packages/kg-default-nodes/lib/nodes/paywall/paywall-parser.js
- packages/kg-default-nodes/lib/utils/is-unsplash-image.js
- packages/kg-default-nodes/lib/nodes/markdown/markdown-renderer.js
- packages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-parser.js
- packages/kg-default-nodes/index.js
- packages/kg-default-nodes/lib/nodes/at-link/index.js
- packages/kg-default-nodes/lib/nodes/codeblock/codeblock-parser.js
- packages/kg-default-nodes/test/test-utils/assertions.js
- packages/kg-default-nodes/test/test-utils/index.js
- packages/kg-default-nodes/lib/kg-default-nodes.js
- packages/kg-default-nodes/rollup.config.mjs
- packages/kg-default-nodes/lib/nodes/html/html-parser.js
- packages/kg-default-nodes/test/test-utils/overrides.js
✅ Files skipped from review due to trivial changes (40)
- packages/kg-default-nodes/src/svg.d.ts
- packages/kg-default-nodes/src/utils/escape-html.ts
- packages/kg-default-nodes/src/serializers/paragraph.ts
- packages/kg-default-nodes/src/utils/render-empty-container.ts
- packages/kg-default-nodes/src/utils/render-helpers/email-button.ts
- packages/kg-default-nodes/src/utils/is-unsplash-image.ts
- packages/kg-default-nodes/src/utils/slugify.ts
- packages/kg-default-nodes/src/utils/is-local-content-image.ts
- packages/kg-default-nodes/test/utils/tagged-template-fns.test.ts
- packages/kg-default-nodes/src/nodes/at-link/index.ts
- packages/kg-default-nodes/src/index.ts
- packages/kg-default-nodes/src/nodes/email/EmailNode.ts
- packages/kg-default-nodes/src/nodes/horizontalrule/HorizontalRuleNode.ts
- packages/kg-default-nodes/src/nodes/image/ImageNode.ts
- packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts
- packages/kg-default-nodes/src/nodes/paywall/PaywallNode.ts
- packages/kg-default-nodes/src/utils/get-available-image-widths.ts
- packages/kg-default-nodes/src/nodes/paywall/paywall-parser.ts
- packages/kg-default-nodes/src/utils/build-clean-basic-html-for-element.ts
- packages/kg-default-nodes/src/utils/clean-dom.ts
- packages/kg-default-nodes/test/nodes/at-link-search.test.ts
- packages/kg-default-nodes/test/nodes/codeblock.test.ts
- packages/kg-default-nodes/test/nodes/embed.test.ts
- packages/kg-default-nodes/test/nodes/file.test.ts
- packages/kg-default-nodes/test/test-utils/index.ts
- packages/kg-default-nodes/test/test-utils/assertions.ts
- packages/kg-default-nodes/test/nodes/video.test.ts
- packages/kg-default-nodes/test/test-utils/overrides.ts
- packages/kg-default-nodes/tsconfig.test.json
- packages/kg-default-nodes/test/test-utils/html-minifier.d.ts
- packages/kg-default-nodes/test/test-utils/should.d.ts
- packages/kg-default-nodes/test/utils/rgb-to-hex.test.ts
- packages/kg-default-nodes/test/test-utils/should-assertions.d.ts
- packages/kg-default-nodes/src/KoenigDecoratorNode.ts
- packages/kg-default-nodes/test/nodes/html.test.ts
- packages/kg-default-nodes/test/nodes/transistor.test.ts
- packages/kg-default-nodes/tsconfig.json
- packages/kg-default-nodes/src/nodes/bookmark/bookmark-renderer.ts
- packages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.ts
- packages/kg-default-nodes/src/nodes/ExtendedQuoteNode.ts
🚧 Files skipped from review as they are similar to previous changes (75)
- packages/kg-default-nodes/src/utils/add-create-document-option.ts
- packages/kg-default-nodes/src/serializers/linebreak.ts
- packages/kg-default-nodes/test/nodes/aside.test.ts
- packages/kg-default-nodes/test/serializers/paragraph.test.ts
- packages/kg-default-nodes/src/nodes/email-cta/email-cta-renderer.ts
- packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts
- packages/kg-default-nodes/src/nodes/file/file-renderer.ts
- packages/kg-default-nodes/src/nodes/file/FileNode.ts
- packages/kg-default-nodes/src/nodes/header/HeaderNode.ts
- packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-parser.ts
- packages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.ts
- packages/kg-default-nodes/src/nodes/header/parsers/header-parser.ts
- packages/kg-default-nodes/src/nodes/toggle/ToggleNode.ts
- packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-renderer.ts
- packages/kg-default-nodes/src/nodes/toggle/toggle-parser.ts
- packages/kg-default-nodes/src/nodes/markdown/MarkdownNode.ts
- packages/kg-default-nodes/src/nodes/signup/signup-parser.ts
- packages/kg-default-nodes/src/nodes/image/image-parser.ts
- packages/kg-default-nodes/src/nodes/transistor/TransistorNode.ts
- packages/kg-default-nodes/src/nodes/html/HtmlNode.ts
- packages/kg-default-nodes/src/nodes/zwnj/ZWNJNode.ts
- packages/kg-default-nodes/src/nodes/product/ProductNode.ts
- packages/kg-default-nodes/src/nodes/html/html-renderer.ts
- packages/kg-default-nodes/src/nodes/signup/signup-renderer.ts
- packages/kg-default-nodes/src/nodes/video/video-renderer.ts
- packages/kg-default-nodes/src/utils/read-caption-from-element.ts
- packages/kg-default-nodes/src/nodes/image/image-renderer.ts
- packages/kg-default-nodes/src/utils/read-image-attributes-from-element.ts
- packages/kg-default-nodes/src/utils/replacement-strings.ts
- packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts
- packages/kg-default-nodes/src/utils/rgb-to-hex.ts
- packages/kg-default-nodes/test/nodes/at-link.test.ts
- packages/kg-default-nodes/src/utils/srcset-attribute.ts
- packages/kg-default-nodes/src/utils/truncate.ts
- packages/kg-default-nodes/src/utils/size-byte-converter.ts
- packages/kg-default-nodes/test/nodes/bookmark.test.ts
- packages/kg-default-nodes/test/nodes/callout.test.ts
- packages/kg-default-nodes/test/nodes/audio.test.ts
- packages/kg-default-nodes/test/nodes/header.test.ts
- packages/kg-default-nodes/test/nodes/horizontalrule.test.ts
- packages/kg-default-nodes/test/nodes/button.test.ts
- packages/kg-default-nodes/test/nodes/zwnj.test.ts
- packages/kg-default-nodes/src/nodes/ExtendedTextNode.ts
- packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts
- packages/kg-default-nodes/src/nodes/aside/AsideNode.ts
- packages/kg-default-nodes/tsconfig.cjs.json
- packages/kg-default-nodes/src/nodes/ExtendedHeadingNode.ts
- packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts
- packages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.ts
- packages/kg-default-nodes/src/nodes/codeblock/codeblock-parser.ts
- packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts
- packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts
- packages/kg-default-nodes/src/nodes/html/html-parser.ts
- packages/kg-default-nodes/test/generate-decorator-node.test.ts
- packages/kg-default-nodes/src/utils/visibility.ts
- packages/kg-default-nodes/test/nodes/email-cta.test.ts
- packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts
- packages/kg-default-nodes/test/nodes/markdown.test.ts
- packages/kg-default-nodes/src/kg-default-nodes.ts
- packages/kg-default-nodes/src/nodes/audio/AudioNode.ts
- packages/kg-default-nodes/src/nodes/audio/audio-parser.ts
- packages/kg-default-nodes/src/nodes/TKNode.ts
- packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts
- packages/kg-default-nodes/src/nodes/button/ButtonNode.ts
- packages/kg-default-nodes/src/nodes/button/button-parser.ts
- packages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.ts
- packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts
- packages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.ts
- packages/kg-default-nodes/src/nodes/callout/callout-parser.ts
- packages/kg-default-nodes/src/utils/read-text-content.ts
- packages/kg-default-nodes/test/nodes/image.test.ts
- packages/kg-default-nodes/src/nodes/video/video-parser.ts
- packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts
- packages/kg-default-nodes/test/nodes/gallery.test.ts
- packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts
c1fdfd3 to
134858b
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1797 +/- ##
==========================================
+ Coverage 86.13% 86.66% +0.52%
==========================================
Files 194 195 +1
Lines 13177 14042 +865
Branches 1962 2085 +123
==========================================
+ Hits 11350 12169 +819
- Misses 1810 1844 +34
- Partials 17 29 +12 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
packages/kg-default-nodes/src/nodes/product/product-parser.ts (1)
25-31:⚠️ Potential issue | 🟠 MajorParse
width/heightattributes into numbers before assigning.Lines 26 and 30 assign string DOM attributes (
img.getAttribute('width')andimg.getAttribute('height')) directly into fields typed asnumber | nullin the ProductNode interface. This creates a runtime type mismatch and leaks incorrect types into the node payload.💡 Proposed fix
if (img && img.getAttribute('src')) { payload.productImageSrc = img.getAttribute('src'); - if (img.getAttribute('width')) { - payload.productImageWidth = img.getAttribute('width'); - } + const widthAttr = img.getAttribute('width'); + if (widthAttr) { + const width = Number.parseInt(widthAttr, 10); + if (!Number.isNaN(width)) { + payload.productImageWidth = width; + } + } - if (img.getAttribute('height')) { - payload.productImageHeight = img.getAttribute('height'); - } + const heightAttr = img.getAttribute('height'); + if (heightAttr) { + const height = Number.parseInt(heightAttr, 10); + if (!Number.isNaN(height)) { + payload.productImageHeight = height; + } + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/product/product-parser.ts` around lines 25 - 31, The code assigns string DOM attributes directly to numeric payload fields: when reading img.getAttribute('width') and img.getAttribute('height') in product-parser.ts (the logic that sets payload.productImageWidth and payload.productImageHeight), parse the attribute values into numbers (e.g., via Number(...) or parseInt(..., 10)) and only assign if the result is a finite number; otherwise set the fields to null to preserve the ProductNode type (number | null) and avoid NaN leaks.packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts (1)
52-56:⚠️ Potential issue | 🔴 CriticalFix color-token validation: current regex is bypassable and email path skips buttonColor validation.
Line 54 and Line 397 use a partially anchored regex, so invalid strings can still pass if they start with allowed chars. Those values are then interpolated into HTML attributes/styles (Line 99+, Line 220, Line 333, Line 353), which can enable attribute/style injection. Also,
buttonColorvalidation runs only inctaCardTemplate(), so the email path never gets that check.Suggested hardening patch
+const COLOR_TOKEN_RE = /^(?:[a-zA-Z\d-]+|#(?:[a-fA-F\d]{3}|[a-fA-F\d]{6}))$/; + +function sanitizeColorToken(value: string, fallback: string) { + return COLOR_TOKEN_RE.test(value) ? value : fallback; +} + function ctaCardTemplate(dataset: CTADataset) { - // Add validation for buttonColor - if (!dataset.buttonColor || !dataset.buttonColor.match(/^[a-zA-Z\d-]+|#([a-fA-F\d]{3}|[a-fA-F\d]{6})$/)) { - dataset.buttonColor = 'accent'; - } const buttonAccent = dataset.buttonColor === 'accent' ? 'kg-style-accent' : ''; @@ export function renderCallToActionNode(node: CTANodeData, options: CTARenderOptions = {}) { @@ - if (!dataset.backgroundColor || !dataset.backgroundColor.match(/^[a-zA-Z\d-]+|#([a-fA-F\d]{3}|[a-fA-F\d]{6})$/)) { - dataset.backgroundColor = 'white'; - } + dataset.buttonColor = sanitizeColorToken(dataset.buttonColor, 'accent'); + dataset.backgroundColor = sanitizeColorToken(dataset.backgroundColor, 'white');Also applies to: 96-102, 397-399
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts` around lines 52 - 56, The buttonColor validation is insecure and inconsistently applied: replace the partially-anchored regex in ctaCardTemplate with a fully-anchored pattern that only allows either an allowed token (letters, digits, hyphen) or a hex color (`#abc` or `#aabbcc`) (e.g. ^(?:[A-Za-z0-9-]+|#(?:[A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}))$), and centralize the check into a single helper (e.g., validateButtonColor) called from ctaCardTemplate and the email-generation path so all interpolations (places referencing dataset.buttonColor in the renderer and email code) use the sanitized/validated value before being placed into attributes/styles to prevent attribute/style injection. Ensure fallback to a safe default ('accent') when validation fails.packages/kg-default-nodes/src/nodes/embed/embed-parser.ts (1)
82-101:⚠️ Potential issue | 🟡 MinorAvoid mutating the DOM element's
srcattribute.Line 91 mutates
iframe.srcdirectly, which modifies the original DOM element. While unlikely to cause issues in typical import scenarios, mutating passed-in DOM elements can lead to subtle side effects if the element is still live in the document.Suggested fix using a local variable
function _createPayloadForIframe(iframe: HTMLIFrameElement) { // If we don't have a src Or it's not an absolute URL, we can't handle this // This regex handles http://, https:// or // if (!iframe.src || !iframe.src.match(/^(https?:)?\/\//i)) { return; } + let url = iframe.src; // if it's a schemaless URL, convert to https - if (iframe.src.match(/^\/\//)) { - iframe.src = `https:${iframe.src}`; + if (url.match(/^\/\//)) { + url = `https:${url}`; } const payload: Record<string, unknown> = { - url: iframe.src + url: url }; payload.html = iframe.outerHTML; return payload; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/embed/embed-parser.ts` around lines 82 - 101, The _createPayloadForIframe function currently mutates the passed-in DOM element by assigning iframe.src = `https:${iframe.src}` for schemaless URLs; change this to use a local variable (e.g., const src = iframe.src || '') and normalize that local src (prefix with "https:" when it starts with "//") and use that local src when building payload.url and payload.html, leaving iframe.src unchanged; keep existing validation using the local src when checking the regex.
♻️ Duplicate comments (5)
packages/kg-default-nodes/src/nodes/markdown/markdown-renderer.ts (1)
19-23:⚠️ Potential issue | 🔴 CriticalSanitize rendered markdown HTML before
innerHTMLassignment.Line 22 writes renderer output directly to
innerHTML. Ifnode.markdownis untrusted, this is an XSS sink. Please sanitizehtmlbefore assignment (or explicitly enforce/document trusted-only markdown upstream).🔧 Example fix (sanitization before DOM injection)
+import DOMPurify from 'isomorphic-dompurify'; import {addCreateDocumentOption} from '../../utils/add-create-document-option.js'; import {render} from '@tryghost/kg-markdown-html-renderer'; @@ - const html = render(node.markdown || '', options as Record<string, unknown>); + const unsafeHtml = render(node.markdown || '', options as Record<string, unknown>); + const html = DOMPurify.sanitize(unsafeHtml); @@ const element = document.createElement('div'); element.innerHTML = html;In the currently used version of `@tryghost/kg-markdown-html-renderer`, does render() sanitize HTML output by default? Please cite the official source or implementation.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/markdown/markdown-renderer.ts` around lines 19 - 23, The renderer currently writes untrusted HTML directly into the DOM (render(node.markdown || '') → element.innerHTML = html), creating an XSS sink; fix by sanitizing the output of render() before assigning to element.innerHTML (e.g. call a sanitizer like DOMPurify or your project's sanitizeHtml helper on the html string and assign the sanitized result), update the import/usage in markdown-renderer.ts to use the sanitizer function and ensure tests cover untrusted input; alternatively, if you decide to enforce trusted-only input, add explicit validation/documentation and defensive checks around node.markdown and render() output instead of direct assignment.packages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.ts (1)
40-41:⚠️ Potential issue | 🔴 CriticalReplace HTML caption injection with text assignment.
Line 41 uses
innerHTMLwithnode.caption, which is an XSS sink for untrusted caption content. Use plain text (or explicitly sanitize first).🔒 Safe fix
- figcaption.innerHTML = node.caption; + figcaption.textContent = node.caption;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.ts` around lines 40 - 41, The figcaption is currently populated using innerHTML with node.caption in codeblock-renderer.ts which creates an XSS sink; replace that with a safe text assignment (e.g., set figcaption.textContent or append a text node) or explicitly sanitize node.caption before injection so untrusted caption content cannot execute HTML—locate the figcaption creation and the line setting figcaption.innerHTML and change it to use textContent or a sanitizer.packages/kg-default-nodes/src/nodes/image/image-renderer.ts (1)
124-127:⚠️ Potential issue | 🟠 MajorNon-null assertions may cause runtime errors if
imageOptimizationis undefined.The assertions
options.imageOptimization!.contentImageSizes!assume both properties exist, butimageOptimizationis optional in the interface. The guard at line 124 checksisLocalContentImageandcanTransformImagebut does not verify thatimageOptimizationorcontentImageSizesare defined.🛡️ Proposed defensive check
if (isLocalContentImage(node.src, options.siteUrl) && options.canTransformImage?.(node.src)) { + const contentImageSizes = options.imageOptimization?.contentImageSizes; + if (!contentImageSizes) { + // Cannot determine retina src without image size configuration + return {element: figure}; + } // find available image size next up from 2x600 so we can use it for the "retina" src - const availableImageWidths = getAvailableImageWidths(node, options.imageOptimization!.contentImageSizes!); + const availableImageWidths = getAvailableImageWidths(node, contentImageSizes);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/image/image-renderer.ts` around lines 124 - 127, The code uses non-null assertions options.imageOptimization!.contentImageSizes! when calling getAvailableImageWidths inside the isLocalContentImage/canTransformImage branch; add a defensive check that options.imageOptimization and options.imageOptimization.contentImageSizes are defined before using them (or bail/skip the transformation if not), and then pass the verified contentImageSizes to getAvailableImageWidths; update the condition that starts with isLocalContentImage(...) && options.canTransformImage?.(node.src) to also require options.imageOptimization && options.imageOptimization.contentImageSizes to avoid runtime errors.packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts (1)
401-407:⚠️ Potential issue | 🟠 MajorSanitize
sponsorLabelbefore the email early-return path.Line 401 returns for
target === 'email'before the sanitizer at Line 412 runs, soemailCTATemplate()still gets rawdataset.sponsorLabel.Suggested fix
export function renderCallToActionNode(node: CTANodeData, options: CTARenderOptions = {}) { @@ const dataset = { @@ linkColor: node.linkColor }; + if (dataset.hasSponsorLabel) { + const cleanBasicHtml = buildCleanBasicHtmlForElement(document.createElement('div')); + const cleanedHtml = cleanBasicHtml(dataset.sponsorLabel, {firstChildInnerContent: true}); + dataset.sponsorLabel = cleanedHtml || ''; + } + if (options.target === 'email') { const emailDoc = options.createDocument!(); const emailDiv = emailDoc.createElement('div'); @@ - if (dataset.hasSponsorLabel) { - const cleanBasicHtml = buildCleanBasicHtmlForElement(element); - const cleanedHtml = cleanBasicHtml(dataset.sponsorLabel, {firstChildInnerContent: true}); - dataset.sponsorLabel = cleanedHtml || ''; - } const htmlString = ctaCardTemplate(dataset);Also applies to: 412-416
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts` around lines 401 - 407, The email branch returns early when options.target === 'email' and calls emailCTATemplate(dataset, options) before dataset.sponsorLabel is sanitized; move or apply the same sanitizer that is currently executed after the email branch so dataset.sponsorLabel is sanitized before calling emailCTATemplate (either sanitize dataset.sponsorLabel in place or create a sanitized copy of dataset and pass that to emailCTATemplate), ensuring you reference options.target, emailCTATemplate, and dataset.sponsorLabel when making the change.packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts (1)
51-55:⚠️ Potential issue | 🟠 MajorGuard thumbnail dimensions before computing aspect ratio.
The non-null assertions on
metadata.thumbnail_width!andmetadata.thumbnail_height!are unsafe. The guard on line 51 only checks forthumbnail_url, not the width/height fields. If these are missing or zero, this will produceNaNorInfinityforthumbnailAspectRatio, generating invalidspacerHeightand broken email HTML.Suggested fix
- if (isEmail && isVideoWithThumbnail) { + const hasValidThumbnailSize = + typeof metadata.thumbnail_width === 'number' && + typeof metadata.thumbnail_height === 'number' && + metadata.thumbnail_width > 0 && + metadata.thumbnail_height > 0; + + if (isEmail && isVideoWithThumbnail && hasValidThumbnailSize) { const emailTemplateMaxWidth = 600; - const thumbnailAspectRatio = metadata.thumbnail_width! / metadata.thumbnail_height!; + const thumbnailAspectRatio = metadata.thumbnail_width / metadata.thumbnail_height;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts` around lines 51 - 55, The code currently computes thumbnailAspectRatio using metadata.thumbnail_width! and metadata.thumbnail_height! without verifying those fields are present and non-zero; update the isEmail && isVideoWithThumbnail branch to first check that metadata.thumbnail_width and metadata.thumbnail_height are defined and > 0 before computing thumbnailAspectRatio (and only then compute spacerHeight), and provide a safe fallback path (e.g., skip thumbnail sizing or use a default aspect ratio/height) if either value is missing or zero so spacerHeight cannot become NaN/Infinity; refer to the isEmail && isVideoWithThumbnail conditional, metadata.thumbnail_width/metadata.thumbnail_height, thumbnailAspectRatio, and spacerHeight to find and change the logic.
🧹 Nitpick comments (14)
packages/kg-default-nodes/test/serializers/paragraph.test.ts (1)
23-23: Add type annotation toDEFAULT_CONFIGto eliminate assertions at callsites.The
as HTMLConfigcasts on lines 23 and 24 exist becauseDEFAULT_CONFIGis declared as an untyped object literal inpackages/kg-default-nodes/src/kg-default-nodes.ts. Adding a source-level type annotation—such assatisfies HTMLConfig—would allow the compiler to enforce type compatibility at the definition site, eliminating the need for assertions in both test files.export const DEFAULT_CONFIG = { html: { import: { ...serializers.linebreak.import, ...serializers.paragraph.import } } } satisfies { html: HTMLConfig };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/serializers/paragraph.test.ts` at line 23, DEFAULT_CONFIG is declared without a type which forces callers (e.g., createHeadlessEditor in tests) to use `as HTMLConfig` casts; update the DEFAULT_CONFIG declaration in kg-default-nodes.ts to include a source-level type annotation (for example use `satisfies { html: HTMLConfig }` or explicitly type the exported DEFAULT_CONFIG) so the compiler enforces shape at the definition site, then remove the `as HTMLConfig` assertions at callsites like the createHeadlessEditor(...) usages that currently pass DEFAULT_CONFIG.html.packages/kg-default-nodes/src/nodes/product/product-parser.ts (1)
4-4: Tighten constructor typing to returnLexicalNodeand drop the cast.Line 4 allows
unknown, then Line 58 force-casts toLexicalNode. This weakens type guarantees in the parser boundary; type the constructor as producingLexicalNodeand return directly.♻️ Proposed refactor
-export function parseProductNode(ProductNode: new (data: Record<string, unknown>) => unknown) { +export function parseProductNode(ProductNode: new (data: Record<string, unknown>) => LexicalNode) { @@ - const node = new ProductNode(payload); - return {node: node as LexicalNode}; + const node = new ProductNode(payload); + return {node};Also applies to: 57-59
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/product/product-parser.ts` at line 4, The parseProductNode signature currently accepts ProductNode as new (data: Record<string, unknown>) => unknown and then force-casts instances to LexicalNode; tighten this by changing the constructor type to new (data: Record<string, unknown>) => LexicalNode in the parseProductNode parameter so you can construct and return the instance as a LexicalNode without casting, and update the other similar spot referenced (lines 57-59) to the same constructor type to remove the cast there as well.packages/kg-default-nodes/test/nodes/tk.test.ts (1)
36-36: Remove redundantTKNodecasts for cleaner tests.On Line 36, Line 41, and Line 111,
tkNodeis alreadyTKNodefrom$createTKNode('TK'), so these casts add noise without extra safety.♻️ Suggested cleanup
- (tkNode as TKNode).isTextEntity().should.be.true(); + tkNode.isTextEntity().should.be.true(); - (tkNode as TKNode).canInsertTextBefore().should.be.false(); + tkNode.canInsertTextBefore().should.be.false(); - const clonedNode = TKNode.clone(tkNode as TKNode); + const clonedNode = TKNode.clone(tkNode);Also applies to: 41-41, 111-111
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/tk.test.ts` at line 36, Remove the redundant type assertions of tkNode; since tkNode is created via $createTKNode('TK') it's already a TKNode, so replace occurrences of (tkNode as TKNode).isTextEntity() (and similar casts around tkNode at places where you created it) with tkNode.isTextEntity(), and remove the extra (tkNode as TKNode) casts in the tests referencing tkNode, $createTKNode and isTextEntity.packages/kg-default-nodes/src/nodes/product/product-renderer.ts (2)
96-104: Type assertions assume dimensions are already numbers.The
as numberassertions on lines 98-99 and 102 bypass runtime type checking. IfgetDataset()returns string values (e.g., from DOM attributes), these assertions will silently pass but cause incorrect behavior at runtime.Consider adding defensive numeric coercion:
🛡️ Proposed fix for defensive numeric coercion
if (data.productImageWidth && data.productImageHeight) { + const width = Number(data.productImageWidth); + const height = Number(data.productImageHeight); + if (!Number.isFinite(width) || !Number.isFinite(height)) { + // Skip dimension calculations if values aren't valid numbers + } else { imageDimensions = { - width: data.productImageWidth as number, - height: data.productImageHeight as number + width, + height }; - if ((data.productImageWidth as number) >= 560) { + if (width >= 560) { imageDimensions = getResizedImageDimensions(imageDimensions, {width: 560}); } + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/product/product-renderer.ts` around lines 96 - 104, The code uses type assertions (data.productImageWidth as number / data.productImageHeight as number) which skip runtime validation; change to defensively coerce and validate values before using them: read numeric values from data.productImageWidth and data.productImageHeight via Number(...) or parseFloat, check isFinite (or !Number.isNaN) and fall back to a safe default or skip imageDimensions if invalid, then use those validated numeric variables for the imageDimensions assignment and for the comparison that decides whether to call getResizedImageDimensions; update references to imageDimensions, getResizedImageDimensions, and the productImageWidth/productImageHeight reads so all numeric operations use the validated coerced numbers.
5-9: Interface may be incomplete compared to actual usage.The
ProductNodeDatainterface only declaresproductStarRating,isEmpty(), andgetDataset(), but the template functions access many more properties (likeproductTitle,productDescription,productImageSrc, etc.) throughgetDataset(). Consider documenting that the full data shape is returned bygetDataset().🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/product/product-renderer.ts` around lines 5 - 9, ProductNodeData currently only lists productStarRating, isEmpty(), and getDataset(), but template code expects many specific fields inside the dataset (e.g., productTitle, productDescription, productImageSrc, etc.); update the type returned by getDataset() to a concrete ProductDataset interface (or expand ProductNodeData) that lists all expected keys (productTitle, productDescription, productImageSrc, productPrice, productUrl, etc.) and use that type for getDataset(): () => ProductDataset so callers and template functions have proper typing and documentation; ensure ProductDataset is exported/placed near ProductNodeData and update any usages of getDataset() accordingly.packages/kg-default-nodes/test/nodes/video.test.ts (1)
96-100: Inconsistentshouldinvocation pattern.Line 96 uses an explicit cast
(should as unknown as (obj: unknown) => should.Assertion)(videoNode.width)while Line 100 usesshould(videoNode.height)directly. Both work, but the inconsistency is confusing. Consider using the same pattern for both null checks.♻️ Suggested consistency fix
- (should as unknown as (obj: unknown) => should.Assertion)(videoNode.width).equal(null); + should(videoNode.width).equal(null); videoNode.width = 600; videoNode.width.should.equal(600); should(videoNode.height).equal(null);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/video.test.ts` around lines 96 - 100, The null assertions for videoNode.width and videoNode.height use mixed should invocation styles; make them consistent by changing the explicit cast invocation for videoNode.width to the same pattern used for videoNode.height (use should(videoNode.width).equal(null)), or vice versa—apply the chosen style to both checks around the videoNode.width and videoNode.height assertions in the video.test.ts test block so both null checks use the identical should(...) form.packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts (1)
4-7: Consider stronger typing forimagesarray in future iteration.The
images: unknown[]type removesanyas intended, but theGalleryImageinterface defined ingallery-renderer.tscould be extracted and reused here for better type safety. This is a minor improvement opportunity, not a blocker.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts` around lines 4 - 7, The GalleryNode interface currently types images as unknown[]; replace this with the concrete GalleryImage type used in gallery-renderer.ts to improve type safety by importing or extracting the GalleryImage interface and updating GalleryNode.images to GalleryImage[] (ensure you update any references or imports for GalleryNode to compile).packages/kg-default-nodes/test/nodes/gallery.test.ts (1)
637-639: Consider creating a typed interface for export options.The
exportOptions as unknown as LexicalEditorcast is used throughout the test file. While this works, it suggests theexportDOMmethod signature might benefit from a union type or a dedicated options interface to improve type safety. This pattern is consistent across test files, so it's not blocking, but worth noting for future improvement.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/gallery.test.ts` around lines 637 - 639, The test is using a double-cast "exportOptions as unknown as LexicalEditor" when calling galleryNode.exportDOM; replace this with a proper typed interface or union: define a dedicated ExportDOMOptions type (or union type that includes LexicalEditor) and type the exportOptions variable accordingly, or update the exportDOM signature to accept ExportDOMOptions | LexicalEditor so you can pass exportOptions without unsafe casting; ensure references use exportDOM, galleryNode, exportOptions and LexicalEditor so the new type is used across this test file (and replicable in other tests).packages/kg-default-nodes/test/nodes/bookmark.test.ts (4)
397-401: Extract node variable to avoid repeated casts.The cast
nodes[0] as BookmarkNodeis repeated for each assertion. Extract it once for consistency with the pattern used in line 375.♻️ Suggested refactor
nodes.length.should.equal(1); -(nodes[0] as BookmarkNode).url.should.equal('https://slack.engineering/typescript-at-slack-a81307fa288d'); -(nodes[0] as BookmarkNode).title.should.equal('TypeScript at Slack'); -(nodes[0] as BookmarkNode).description.should.equal('Or, How I Learned to Stop Worrying & Trust the Compiler'); -(nodes[0] as BookmarkNode).publisher.should.equal('slack.engineering'); -(nodes[0] as BookmarkNode).thumbnail.should.equal('https://cdn-images-1.medium.com/fit/c/160/160/1*-h1bH8gB3I7gPh5AG1HmsQ.png'); +const node = nodes[0] as BookmarkNode; +node.url.should.equal('https://slack.engineering/typescript-at-slack-a81307fa288d'); +node.title.should.equal('TypeScript at Slack'); +node.description.should.equal('Or, How I Learned to Stop Worrying & Trust the Compiler'); +node.publisher.should.equal('slack.engineering'); +node.thumbnail.should.equal('https://cdn-images-1.medium.com/fit/c/160/160/1*-h1bH8gB3I7gPh5AG1HmsQ.png');🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/bookmark.test.ts` around lines 397 - 401, Extract nodes[0] once into a local typed variable to avoid repeated casts: assign const node = nodes[0] as BookmarkNode (or similar) and then use node.url, node.title, node.description, node.publisher, node.thumbnail in the assertions; update the assertions that currently reference (nodes[0] as BookmarkNode) to use the new node variable and keep all assertion texts unchanged.
166-166: Consider adding a clarifying comment for the double cast pattern.The
as unknown as LexicalEditorcast is used becauseexportDOMexpects aLexicalEditorper Lexical's signature, but this implementation actually consumes render options. A brief comment explaining this type mismatch would help future maintainers understand why the cast is necessary.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/bookmark.test.ts` at line 166, Add a brief inline comment next to the double-cast on the exportDOM call (bookmarkNode.exportDOM(exportOptions as unknown as LexicalEditor)) explaining that exportDOM's TypeScript signature expects a LexicalEditor but this implementation actually passes render/export options, so the "as unknown as LexicalEditor" cast is intentional to satisfy the signature; reference exportOptions and bookmarkNode.exportDOM in the comment for clarity.
375-383: Reuse themetadatavariable already declared on line 352.The
metadatavariable is extracted on line 352 but not reused for these assertions. This would eliminate the repeated inline casts.♻️ Suggested refactor
const node = nodes[0] as BookmarkNode; node.url.should.equal(dataset.url); -node.icon.should.equal((dataset.metadata as Record<string, unknown>).icon); -node.title.should.equal((dataset.metadata as Record<string, unknown>).title); -node.description.should.equal((dataset.metadata as Record<string, unknown>).description); -node.author.should.equal((dataset.metadata as Record<string, unknown>).author); -node.publisher.should.equal((dataset.metadata as Record<string, unknown>).publisher); -node.thumbnail.should.equal((dataset.metadata as Record<string, unknown>).thumbnail); +node.icon.should.equal(metadata.icon); +node.title.should.equal(metadata.title); +node.description.should.equal(metadata.description); +node.author.should.equal(metadata.author); +node.publisher.should.equal(metadata.publisher); +node.thumbnail.should.equal(metadata.thumbnail); node.caption.should.equal(dataset.caption);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/bookmark.test.ts` around lines 375 - 383, In the assertions block for the BookmarkNode (variables node and dataset), replace the repeated inline casts of (dataset.metadata as Record<string, unknown>) with the already-extracted metadata variable declared earlier (line 352) — e.g., use metadata.icon, metadata.title, metadata.description, metadata.author, metadata.publisher, metadata.thumbnail — while keeping node.url and node.caption comparisons against dataset.url and dataset.caption; update references in the test so all metadata-based assertions use the metadata identifier instead of re-casting dataset.metadata.
320-326: Extractmetadatavariable to reduce repetition.The cast
(dataset.metadata as Record<string, unknown>)is repeated 6 times here. Extract it once for cleaner assertions, similar to the pattern used in line 62.♻️ Suggested refactor
editor.getEditorState().read(() => { try { const [bookmarkNode] = $getRoot().getChildren() as BookmarkNode[]; + const metadata = dataset.metadata as Record<string, unknown>; bookmarkNode.url.should.equal(dataset.url); - bookmarkNode.icon.should.equal((dataset.metadata as Record<string, unknown>).icon); - bookmarkNode.title.should.equal((dataset.metadata as Record<string, unknown>).title); - bookmarkNode.description.should.equal((dataset.metadata as Record<string, unknown>).description); - bookmarkNode.author.should.equal((dataset.metadata as Record<string, unknown>).author); - bookmarkNode.publisher.should.equal((dataset.metadata as Record<string, unknown>).publisher); - bookmarkNode.thumbnail.should.equal((dataset.metadata as Record<string, unknown>).thumbnail); + bookmarkNode.icon.should.equal(metadata.icon); + bookmarkNode.title.should.equal(metadata.title); + bookmarkNode.description.should.equal(metadata.description); + bookmarkNode.author.should.equal(metadata.author); + bookmarkNode.publisher.should.equal(metadata.publisher); + bookmarkNode.thumbnail.should.equal(metadata.thumbnail); bookmarkNode.caption.should.equal(dataset.caption);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/bookmark.test.ts` around lines 320 - 326, Replace the repeated casts by extracting metadata once: create a local const (e.g., meta) assigned to dataset.metadata cast as Record<string, unknown>, then update the assertions to use bookmarkNode.icon.should.equal(meta.icon), bookmarkNode.title.should.equal(meta.title), bookmarkNode.description.should.equal(meta.description), bookmarkNode.author.should.equal(meta.author), bookmarkNode.publisher.should.equal(meta.publisher), and bookmarkNode.thumbnail.should.equal(meta.thumbnail) while leaving bookmarkNode.caption.should.equal(dataset.caption) unchanged; this removes the repeated (dataset.metadata as Record<string, unknown>) occurrences and mirrors the pattern used earlier.packages/kg-default-nodes/test/nodes/transistor.test.ts (1)
49-52: Consider defining a local Visibility type to reduce casting verbosity.The nested casts work but are repetitive. A local type alias could improve readability:
type Visibility = { web: { nonMember: boolean; memberSegment: string }; email: { memberSegment: string }; };Then use
transistorNode.visibility as Visibilityonce per assertion block.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/transistor.test.ts` around lines 49 - 52, Define a local type alias (e.g., Visibility) that matches the expected shape of transistorNode.visibility and use it to reduce repetitive casts: declare type Visibility with web and email subtypes, cast transistorNode.visibility once to Visibility (assign to the existing visibility const or a new typed const) and update the three assertions to reference the typed properties without repeated Record<string, unknown> casts; adjust the assertions that use visibility.web.nonMember, visibility.web.memberSegment, and visibility.email.memberSegment accordingly.packages/kg-default-nodes/test/nodes/header.test.ts (1)
447-451: Sameas unknown as LexicalEditorcast pattern as other tests.This is consistent with the approach across the migration, but the same recommendation applies: consider defining a proper
ExportOptionstype to improve type safety in tests.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/header.test.ts` around lines 447 - 451, Tests use a repeated unsafe double-cast "as unknown as LexicalEditor" when calling headerNode.exportDOM(exportOptions), so define a proper ExportOptions type used by exportDOM calls (or import the existing ExportOptions interface from the node implementation) and construct exportOptions with that type in the test; update the call to headerNode.exportDOM(exportOptions) to use the strongly typed ExportOptions instead of the cast, referencing the headerNode.exportDOM/exportDOM signature and the ExportOptions type to locate where to change the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/kg-default-nodes/package.json`:
- Line 45: The package declares "@types/node": "24.12.0" but lacks an
engines.node field; either add a matching engines.node entry or lower the
`@types/node` version to match supported Node targets. Edit
packages/kg-default-nodes/package.json and do one of: (A) add "engines": {
"node": "^22.13.1 || ^24.0.0" } to align with the other packages, or (B) change
"@types/node" to the same lower version used across the monorepo, then run
install/build to ensure types and CI pass. Ensure package.json's dependency
entry "@types/node": and the new "engines" key are updated consistently.
In `@packages/kg-default-nodes/src/nodes/ExtendedHeadingNode.ts`:
- Around line 36-37: importJSON currently returns a base HeadingNode (via
HeadingNode.importJSON) which breaks the round-trip because exportJSON emits
type 'extended-heading'; change ExtendedHeadingNode.importJSON to construct and
return an ExtendedHeadingNode instance instead of delegating to
HeadingNode.importJSON — e.g., call HeadingNode.importJSON(serializedNode) to
read base properties if useful, then create new ExtendedHeadingNode(...) and
copy over the necessary serialized fields (level/tag/format/children/keys or
other props) so the deserialized object is an ExtendedHeadingNode whose type
matches exportJSON.
In `@packages/kg-default-nodes/src/nodes/ExtendedTextNode.ts`:
- Around line 37-38: The importJSON implementation currently delegates to
TextNode.importJSON and returns a base TextNode, losing the ExtendedTextNode
subclass; update ExtendedTextNode.importJSON(serializedNode: SerializedTextNode)
to construct and return an ExtendedTextNode instance, restore any serialized
properties (text content, format, and any ExtendedTextNode-specific fields) onto
that instance, and avoid calling TextNode.importJSON; follow the pattern used by
TKNode (instantiate the subclass, set state from serializedNode, and return the
subclass instance) so deserialization preserves the extended node type and
behavior.
In `@packages/kg-default-nodes/src/nodes/image/image-renderer.ts`:
- Around line 132-133: The code uses a non-null assertion on
node.src.match(/(.*\/content\/images)\/(.*)/) which can throw if the regex
doesn't match; update the image handling in the image-renderer (the block around
isLocalContentImage, node.src.match, and img.setAttribute) to capture the match
result into a variable (e.g., const match = node.src.match(...)), verify match
is truthy before destructuring imagesPath and filename, and handle the failure
case (skip setting the sized src, fall back to node.src, or log/error) so
img.setAttribute('src', ...) is only called when imagesPath/filename are valid.
Ensure the regex used matches the earlier isLocalContentImage check or make both
consistent.
In `@packages/kg-default-nodes/test/nodes/header.test.ts`:
- Around line 151-160: The test "renders nothing when header and subheader is
undefined and the button is disabled" currently no-ops via `void element` after
calling $createHeaderNode(...).exportDOM(exportOptions as unknown as
LexicalEditor), so it doesn't validate behavior; replace the no-op with a real
assertion or add a TODO and explicit assertion depending on desired approach:
either (A) if you want to track the renderer bug, add a TODO/TICKET comment
above the test and assert that element is null (or assert that exportDOM returns
null) so the intended behavior is documented, or (B) if you want the test to
reflect current behavior, remove the `null as unknown as string` casts and
assert that `element` is an HTMLElement (or not null) returned from
node.exportDOM; reference the test function name and the $createHeaderNode,
node.exportDOM, and exportOptions/LexicalEditor usage when making the change.
In `@packages/kg-default-nodes/test/nodes/transistor.test.ts`:
- Around line 165-166: The test uses an unnecessary cast when calling
transistorNode.exportDOM: remove the "as unknown as LexicalEditor" cast and call
transistorNode.exportDOM(exportOptions) directly (exportOptions is already a
Record<string, unknown> and matches exportDOM's parameter), updating all
occurrences where exportOptions is cast to LexicalEditor so the call uses the
typed exportOptions variable without defeating type safety.
In `@packages/koenig-lexical/test/unit/hooks/useVisibilityToggle.test.js`:
- Around line 8-14: The mock created in the vi.mock call incorrectly spreads
actual.default (which is undefined for the 'lexical' module) causing only
$getNodeByKey to be present; update the mock to spread the real module object
returned by vi.importActual('lexical') (i.e. use ...actual) and then override
$getNodeByKey with vi.fn() so all named Lexical exports remain available while
mocking $getNodeByKey.
---
Outside diff comments:
In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts`:
- Around line 52-56: The buttonColor validation is insecure and inconsistently
applied: replace the partially-anchored regex in ctaCardTemplate with a
fully-anchored pattern that only allows either an allowed token (letters,
digits, hyphen) or a hex color (`#abc` or `#aabbcc`) (e.g.
^(?:[A-Za-z0-9-]+|#(?:[A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}))$), and centralize the
check into a single helper (e.g., validateButtonColor) called from
ctaCardTemplate and the email-generation path so all interpolations (places
referencing dataset.buttonColor in the renderer and email code) use the
sanitized/validated value before being placed into attributes/styles to prevent
attribute/style injection. Ensure fallback to a safe default ('accent') when
validation fails.
In `@packages/kg-default-nodes/src/nodes/embed/embed-parser.ts`:
- Around line 82-101: The _createPayloadForIframe function currently mutates the
passed-in DOM element by assigning iframe.src = `https:${iframe.src}` for
schemaless URLs; change this to use a local variable (e.g., const src =
iframe.src || '') and normalize that local src (prefix with "https:" when it
starts with "//") and use that local src when building payload.url and
payload.html, leaving iframe.src unchanged; keep existing validation using the
local src when checking the regex.
In `@packages/kg-default-nodes/src/nodes/product/product-parser.ts`:
- Around line 25-31: The code assigns string DOM attributes directly to numeric
payload fields: when reading img.getAttribute('width') and
img.getAttribute('height') in product-parser.ts (the logic that sets
payload.productImageWidth and payload.productImageHeight), parse the attribute
values into numbers (e.g., via Number(...) or parseInt(..., 10)) and only assign
if the result is a finite number; otherwise set the fields to null to preserve
the ProductNode type (number | null) and avoid NaN leaks.
---
Duplicate comments:
In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts`:
- Around line 401-407: The email branch returns early when options.target ===
'email' and calls emailCTATemplate(dataset, options) before dataset.sponsorLabel
is sanitized; move or apply the same sanitizer that is currently executed after
the email branch so dataset.sponsorLabel is sanitized before calling
emailCTATemplate (either sanitize dataset.sponsorLabel in place or create a
sanitized copy of dataset and pass that to emailCTATemplate), ensuring you
reference options.target, emailCTATemplate, and dataset.sponsorLabel when making
the change.
In `@packages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.ts`:
- Around line 40-41: The figcaption is currently populated using innerHTML with
node.caption in codeblock-renderer.ts which creates an XSS sink; replace that
with a safe text assignment (e.g., set figcaption.textContent or append a text
node) or explicitly sanitize node.caption before injection so untrusted caption
content cannot execute HTML—locate the figcaption creation and the line setting
figcaption.innerHTML and change it to use textContent or a sanitizer.
In `@packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts`:
- Around line 51-55: The code currently computes thumbnailAspectRatio using
metadata.thumbnail_width! and metadata.thumbnail_height! without verifying those
fields are present and non-zero; update the isEmail && isVideoWithThumbnail
branch to first check that metadata.thumbnail_width and
metadata.thumbnail_height are defined and > 0 before computing
thumbnailAspectRatio (and only then compute spacerHeight), and provide a safe
fallback path (e.g., skip thumbnail sizing or use a default aspect ratio/height)
if either value is missing or zero so spacerHeight cannot become NaN/Infinity;
refer to the isEmail && isVideoWithThumbnail conditional,
metadata.thumbnail_width/metadata.thumbnail_height, thumbnailAspectRatio, and
spacerHeight to find and change the logic.
In `@packages/kg-default-nodes/src/nodes/image/image-renderer.ts`:
- Around line 124-127: The code uses non-null assertions
options.imageOptimization!.contentImageSizes! when calling
getAvailableImageWidths inside the isLocalContentImage/canTransformImage branch;
add a defensive check that options.imageOptimization and
options.imageOptimization.contentImageSizes are defined before using them (or
bail/skip the transformation if not), and then pass the verified
contentImageSizes to getAvailableImageWidths; update the condition that starts
with isLocalContentImage(...) && options.canTransformImage?.(node.src) to also
require options.imageOptimization && options.imageOptimization.contentImageSizes
to avoid runtime errors.
In `@packages/kg-default-nodes/src/nodes/markdown/markdown-renderer.ts`:
- Around line 19-23: The renderer currently writes untrusted HTML directly into
the DOM (render(node.markdown || '') → element.innerHTML = html), creating an
XSS sink; fix by sanitizing the output of render() before assigning to
element.innerHTML (e.g. call a sanitizer like DOMPurify or your project's
sanitizeHtml helper on the html string and assign the sanitized result), update
the import/usage in markdown-renderer.ts to use the sanitizer function and
ensure tests cover untrusted input; alternatively, if you decide to enforce
trusted-only input, add explicit validation/documentation and defensive checks
around node.markdown and render() output instead of direct assignment.
---
Nitpick comments:
In `@packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts`:
- Around line 4-7: The GalleryNode interface currently types images as
unknown[]; replace this with the concrete GalleryImage type used in
gallery-renderer.ts to improve type safety by importing or extracting the
GalleryImage interface and updating GalleryNode.images to GalleryImage[] (ensure
you update any references or imports for GalleryNode to compile).
In `@packages/kg-default-nodes/src/nodes/product/product-parser.ts`:
- Line 4: The parseProductNode signature currently accepts ProductNode as new
(data: Record<string, unknown>) => unknown and then force-casts instances to
LexicalNode; tighten this by changing the constructor type to new (data:
Record<string, unknown>) => LexicalNode in the parseProductNode parameter so you
can construct and return the instance as a LexicalNode without casting, and
update the other similar spot referenced (lines 57-59) to the same constructor
type to remove the cast there as well.
In `@packages/kg-default-nodes/src/nodes/product/product-renderer.ts`:
- Around line 96-104: The code uses type assertions (data.productImageWidth as
number / data.productImageHeight as number) which skip runtime validation;
change to defensively coerce and validate values before using them: read numeric
values from data.productImageWidth and data.productImageHeight via Number(...)
or parseFloat, check isFinite (or !Number.isNaN) and fall back to a safe default
or skip imageDimensions if invalid, then use those validated numeric variables
for the imageDimensions assignment and for the comparison that decides whether
to call getResizedImageDimensions; update references to imageDimensions,
getResizedImageDimensions, and the productImageWidth/productImageHeight reads so
all numeric operations use the validated coerced numbers.
- Around line 5-9: ProductNodeData currently only lists productStarRating,
isEmpty(), and getDataset(), but template code expects many specific fields
inside the dataset (e.g., productTitle, productDescription, productImageSrc,
etc.); update the type returned by getDataset() to a concrete ProductDataset
interface (or expand ProductNodeData) that lists all expected keys
(productTitle, productDescription, productImageSrc, productPrice, productUrl,
etc.) and use that type for getDataset(): () => ProductDataset so callers and
template functions have proper typing and documentation; ensure ProductDataset
is exported/placed near ProductNodeData and update any usages of getDataset()
accordingly.
In `@packages/kg-default-nodes/test/nodes/bookmark.test.ts`:
- Around line 397-401: Extract nodes[0] once into a local typed variable to
avoid repeated casts: assign const node = nodes[0] as BookmarkNode (or similar)
and then use node.url, node.title, node.description, node.publisher,
node.thumbnail in the assertions; update the assertions that currently reference
(nodes[0] as BookmarkNode) to use the new node variable and keep all assertion
texts unchanged.
- Line 166: Add a brief inline comment next to the double-cast on the exportDOM
call (bookmarkNode.exportDOM(exportOptions as unknown as LexicalEditor))
explaining that exportDOM's TypeScript signature expects a LexicalEditor but
this implementation actually passes render/export options, so the "as unknown as
LexicalEditor" cast is intentional to satisfy the signature; reference
exportOptions and bookmarkNode.exportDOM in the comment for clarity.
- Around line 375-383: In the assertions block for the BookmarkNode (variables
node and dataset), replace the repeated inline casts of (dataset.metadata as
Record<string, unknown>) with the already-extracted metadata variable declared
earlier (line 352) — e.g., use metadata.icon, metadata.title,
metadata.description, metadata.author, metadata.publisher, metadata.thumbnail —
while keeping node.url and node.caption comparisons against dataset.url and
dataset.caption; update references in the test so all metadata-based assertions
use the metadata identifier instead of re-casting dataset.metadata.
- Around line 320-326: Replace the repeated casts by extracting metadata once:
create a local const (e.g., meta) assigned to dataset.metadata cast as
Record<string, unknown>, then update the assertions to use
bookmarkNode.icon.should.equal(meta.icon),
bookmarkNode.title.should.equal(meta.title),
bookmarkNode.description.should.equal(meta.description),
bookmarkNode.author.should.equal(meta.author),
bookmarkNode.publisher.should.equal(meta.publisher), and
bookmarkNode.thumbnail.should.equal(meta.thumbnail) while leaving
bookmarkNode.caption.should.equal(dataset.caption) unchanged; this removes the
repeated (dataset.metadata as Record<string, unknown>) occurrences and mirrors
the pattern used earlier.
In `@packages/kg-default-nodes/test/nodes/gallery.test.ts`:
- Around line 637-639: The test is using a double-cast "exportOptions as unknown
as LexicalEditor" when calling galleryNode.exportDOM; replace this with a proper
typed interface or union: define a dedicated ExportDOMOptions type (or union
type that includes LexicalEditor) and type the exportOptions variable
accordingly, or update the exportDOM signature to accept ExportDOMOptions |
LexicalEditor so you can pass exportOptions without unsafe casting; ensure
references use exportDOM, galleryNode, exportOptions and LexicalEditor so the
new type is used across this test file (and replicable in other tests).
In `@packages/kg-default-nodes/test/nodes/header.test.ts`:
- Around line 447-451: Tests use a repeated unsafe double-cast "as unknown as
LexicalEditor" when calling headerNode.exportDOM(exportOptions), so define a
proper ExportOptions type used by exportDOM calls (or import the existing
ExportOptions interface from the node implementation) and construct
exportOptions with that type in the test; update the call to
headerNode.exportDOM(exportOptions) to use the strongly typed ExportOptions
instead of the cast, referencing the headerNode.exportDOM/exportDOM signature
and the ExportOptions type to locate where to change the test.
In `@packages/kg-default-nodes/test/nodes/tk.test.ts`:
- Line 36: Remove the redundant type assertions of tkNode; since tkNode is
created via $createTKNode('TK') it's already a TKNode, so replace occurrences of
(tkNode as TKNode).isTextEntity() (and similar casts around tkNode at places
where you created it) with tkNode.isTextEntity(), and remove the extra (tkNode
as TKNode) casts in the tests referencing tkNode, $createTKNode and
isTextEntity.
In `@packages/kg-default-nodes/test/nodes/transistor.test.ts`:
- Around line 49-52: Define a local type alias (e.g., Visibility) that matches
the expected shape of transistorNode.visibility and use it to reduce repetitive
casts: declare type Visibility with web and email subtypes, cast
transistorNode.visibility once to Visibility (assign to the existing visibility
const or a new typed const) and update the three assertions to reference the
typed properties without repeated Record<string, unknown> casts; adjust the
assertions that use visibility.web.nonMember, visibility.web.memberSegment, and
visibility.email.memberSegment accordingly.
In `@packages/kg-default-nodes/test/nodes/video.test.ts`:
- Around line 96-100: The null assertions for videoNode.width and
videoNode.height use mixed should invocation styles; make them consistent by
changing the explicit cast invocation for videoNode.width to the same pattern
used for videoNode.height (use should(videoNode.width).equal(null)), or vice
versa—apply the chosen style to both checks around the videoNode.width and
videoNode.height assertions in the video.test.ts test block so both null checks
use the identical should(...) form.
In `@packages/kg-default-nodes/test/serializers/paragraph.test.ts`:
- Line 23: DEFAULT_CONFIG is declared without a type which forces callers (e.g.,
createHeadlessEditor in tests) to use `as HTMLConfig` casts; update the
DEFAULT_CONFIG declaration in kg-default-nodes.ts to include a source-level type
annotation (for example use `satisfies { html: HTMLConfig }` or explicitly type
the exported DEFAULT_CONFIG) so the compiler enforces shape at the definition
site, then remove the `as HTMLConfig` assertions at callsites like the
createHeadlessEditor(...) usages that currently pass DEFAULT_CONFIG.html.
134858b to
b83e572
Compare
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
packages/kg-default-nodes/src/nodes/product/product-parser.ts (1)
14-18:⚠️ Potential issue | 🟠 MajorNormalize imported image dimensions to numbers.
getAttribute('width'/'height')returns strings, butpackages/kg-default-nodes/src/nodes/product/ProductNode.tsdefines these fields asnumber | null, andpackages/kg-default-nodes/src/nodes/product/product-renderer.tslater uses them numerically. Leaving them as strings makes imported product cards depend on runtime coercion and can break the resize path.🔧 Suggested fix
const img = domNode.querySelector('.kg-product-card-image'); if (img && img.getAttribute('src')) { payload.productImageSrc = img.getAttribute('src'); - if (img.getAttribute('width')) { - payload.productImageWidth = img.getAttribute('width'); - } + const widthAttr = img.getAttribute('width'); + if (widthAttr) { + const width = Number(widthAttr); + if (Number.isFinite(width)) { + payload.productImageWidth = width; + } + } - if (img.getAttribute('height')) { - payload.productImageHeight = img.getAttribute('height'); - } + const heightAttr = img.getAttribute('height'); + if (heightAttr) { + const height = Number(heightAttr); + if (Number.isFinite(height)) { + payload.productImageHeight = height; + } + } }Also applies to: 21-31
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/product/product-parser.ts` around lines 14 - 18, The imported product image width/height are currently taken from element.getAttribute('width'/'height') which yields strings but ProductNode.ts declares those fields as number | null and product-renderer.ts expects numeric values; update the parsing in product-parser.ts where you build the payload (the properties for product image dimensions) to convert the attribute strings to numbers (e.g., using Number(...) or parseInt(...)) and set them to null when the attribute is missing or not a valid number so payload.productImageWidth/productImageHeight are proper number|null values and avoid runtime coercion issues.packages/kg-default-nodes/src/nodes/video/video-parser.ts (2)
23-27:⚠️ Potential issue | 🟠 MajorRead
cardWidthfrom the<figure>, not the inner<video>.Line 26 passes
videoNodeintogetCardWidth(), but this package renderskg-width-*on the outer<figure>inpackages/kg-default-nodes/src/nodes/video/video-renderer.tsat Line 55 and Lines 166-171. Wide/full video cards will round-trip through HTML import asregular.🔧 Suggested fix
- cardWidth: getCardWidth(videoNode) + cardWidth: getCardWidth(domNode)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/video/video-parser.ts` around lines 23 - 27, The payload is reading cardWidth from the inner video node (videoNode) but the kg-width-* attribute is placed on the outer <figure>, so change the code that builds payload (where payload: { src, loop, cardWidth } is created) to obtain the card width from the figure wrapper instead of videoNode — e.g., locate where getCardWidth(videoNode) is called and call getCardWidth with the outer figure node (or read the kg-width-* attribute from the parent/figure element) so getCardWidth receives the element that actually carries the width class and returns correct width for wide/full cards.
29-35:⚠️ Potential issue | 🟡 MinorReject malformed durations before assigning them.
parseInt()returnsNaNon invalid input and does not throw an exception, so thecatchblock never executes. This allowsNaNto be assigned topayload.duration, which later surfaces asNaN:NaNin theformattedDurationgetter whenMath.floor(NaN)producesNaNandString(NaN)renders as"NaN".Validate parsed values with
Number.isFinite()before assignment:Suggested fix
if (durationText) { const [minutes, seconds] = durationText.split(':'); - try { - payload.duration = parseInt(minutes) * 60 + parseInt(seconds); - } catch { - // ignore duration + const parsedMinutes = Number.parseInt(minutes, 10); + const parsedSeconds = Number.parseInt(seconds, 10); + + if (Number.isFinite(parsedMinutes) && Number.isFinite(parsedSeconds)) { + payload.duration = (parsedMinutes * 60) + parsedSeconds; } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/video/video-parser.ts` around lines 29 - 35, The duration parsing currently uses parseInt on durationText parts and assigns directly to payload.duration, but parseInt returns NaN for bad input and never throws, so invalid values get assigned; update the logic in video-parser.ts (the block handling durationText and payload.duration) to parse minutes and seconds, validate both with Number.isFinite (or Number.isFinite(Number(...))) and only assign payload.duration when both parts are finite numbers; if validation fails, do not set payload.duration (or leave it undefined/null) so formattedDuration getter won't produce "NaN:NaN".packages/kg-default-nodes/src/generate-decorator-node.ts (1)
121-129:⚠️ Potential issue | 🟠 MajorConstructor and
getPropertyDefaults()share mutable defaults and collapse falsy values.The constructor at line 121 uses
||for non-boolean defaults, which collapses valid falsy values ('',0,null). Additionally,getPropertyDefaults()at line 155 returns rawprop.defaultvalues without cloning, causing object and array defaults to be shared by reference across instances. For example,GalleryNodedefinesimageswith default[], so two gallery nodes will share the same backing array until reassigned. Switch constructor to??(matching the boolean case at line 124) and clone non-primitive defaults ingetPropertyDefaults().🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/generate-decorator-node.ts` around lines 121 - 129, The constructor currently uses || for non-boolean defaults which collapses valid falsy values; change the fallback logic in the constructor (inside the class constructor that iterates internalProps and assigns this[prop.privateName]) to use the nullish coalescing operator (??) for non-boolean defaults just like the boolean branch, and ensure getPropertyDefaults() clones non-primitive default values (arrays/objects) before returning them so instances do not share references (e.g., GalleryNode.images default [] should be cloned per instance); use structuredClone or an appropriate deep-clone utility for prop.default when typeof prop.default is 'object' or Array.isArray(prop.default).packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts (1)
53-60:⚠️ Potential issue | 🔴 CriticalFully anchor the color validators before interpolating them into HTML attributes.
The regex
/^[a-zA-Z\d-]+|#([a-fA-F\d]{3}|[a-fA-F\d]{6})$/only anchors the hex pattern with$; the first alternative lacks an end anchor. This allows values likered" onclick="..."to pass validation and break out ofstyleandclassattributes, creating a stored-XSS vulnerability affecting bothbuttonColor(line 60) andbackgroundColor(line 63).Fix
- if (!dataset.buttonColor || !dataset.buttonColor.match(/^[a-zA-Z\d-]+|#([a-fA-F\d]{3}|[a-fA-F\d]{6})$/)) { + if (!dataset.buttonColor || !/^(?:[a-zA-Z\d-]+|#(?:[a-fA-F\d]{3}|[a-fA-F\d]{6}))$/.test(dataset.buttonColor)) { dataset.buttonColor = 'accent'; } @@ - if (!dataset.backgroundColor || !dataset.backgroundColor.match(/^[a-zA-Z\d-]+|#([a-fA-F\d]{3}|[a-fA-F\d]{6})$/)) { + if (!dataset.backgroundColor || !/^(?:[a-zA-Z\d-]+|#(?:[a-fA-F\d]{3}|[a-fA-F\d]{6}))$/.test(dataset.backgroundColor)) { dataset.backgroundColor = 'white'; }Also applies to: 397-399
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts` around lines 53 - 60, The current validator for dataset.buttonColor uses an alternation where only the hex branch is fully anchored, letting values like `red" onclick="..."` bypass validation; update the check in calltoaction-renderer.ts so the entire alternation is anchored (e.g. use a single regex that wraps both branches with ^...$ such as /^(?:[a-zA-Z\d-]+|#(?:[a-fA-F\d]{3}|[a-fA-F\d]{6}))$/) and apply the same anchored regex to the backgroundColor validation; keep the fallback to 'accent' when validation fails and ensure buttonAccent and buttonStyle continue to derive from the sanitized dataset.buttonColor and dataset.buttonTextColor.
♻️ Duplicate comments (6)
packages/kg-default-nodes/src/nodes/ExtendedHeadingNode.ts (1)
36-38:⚠️ Potential issue | 🟠 Major
importJSONstill returns baseHeadingNodeinstead ofExtendedHeadingNode.This issue was flagged in a previous review but remains unaddressed. Delegating to
HeadingNode.importJSON(serializedNode)returns aHeadingNodeinstance, whileexportJSON()at line 42 setstypeto'extended-heading'. This breaks serialization round-trip integrity.🐛 Proposed fix
static importJSON(serializedNode: SerializedHeadingNode) { - return HeadingNode.importJSON(serializedNode); + const node = new ExtendedHeadingNode(serializedNode.tag); + node.setDirection(serializedNode.direction); + node.setFormat(serializedNode.format); + node.setIndent(serializedNode.indent); + return node; }Alternatively, if
updateFromJSONis available in the Lexical version being used:static importJSON(serializedNode: SerializedHeadingNode) { - return HeadingNode.importJSON(serializedNode); + return new ExtendedHeadingNode(serializedNode.tag).updateFromJSON(serializedNode); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/ExtendedHeadingNode.ts` around lines 36 - 38, The importJSON implementation for ExtendedHeadingNode currently delegates to HeadingNode.importJSON and returns a HeadingNode, breaking round-trip serialization with exportJSON (which sets type 'extended-heading'); change ExtendedHeadingNode.importJSON to construct and return an ExtendedHeadingNode instance by either calling ExtendedHeadingNode.updateFromJSON(serializedNode, node) if updateFromJSON is available or by creating a new ExtendedHeadingNode and copying relevant properties from serializedNode (level, text/content, and any extended props) so the returned object is an ExtendedHeadingNode that matches exportJSON's type.packages/kg-default-nodes/src/nodes/file/file-parser.ts (1)
16-22:⚠️ Potential issue | 🟡 MinorKeep
fileSizeas a string to matchFileNode’s contract.On Line [16],
sizeToBytes()returnsnumber, butpackages/kg-default-nodes/src/nodes/file/FileNode.tsdefinesfileSizeasstring. This still introduces a type-contract mismatch in the parser payload.Proposed fix
- const fileSize = sizeToBytes(domNode.querySelector('.kg-file-card-filesize')?.textContent || ''); + const fileSize = String(sizeToBytes(domNode.querySelector('.kg-file-card-filesize')?.textContent || ''));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/file/file-parser.ts` around lines 16 - 22, The parser sets fileSize by calling sizeToBytes(domNode.querySelector('.kg-file-card-filesize')?.textContent || '') which returns a number, but FileNode.ts defines fileSize as a string; change the parser (in file-parser.ts) to preserve fileSize as a string (e.g., keep the original textContent or convert the numeric result back to the same string format expected by FileNode) so the payload sent from the parser uses a string for fileSize; update the variable fileSize and the payload construction (src, fileTitle, fileCaption, fileName, fileSize) accordingly to match FileNode's contract.packages/kg-default-nodes/src/nodes/ExtendedTextNode.ts (1)
37-39:⚠️ Potential issue | 🟠 Major
importJSONstill delegates toTextNode.importJSON(), breaking round-trip serialization.This was previously flagged. The implementation returns a base
TextNodeinstead ofExtendedTextNode, so deserialized nodes lose their subclass type. SinceexportJSON()setstype: 'extended-text',importJSONshould return anExtendedTextNodeinstance.Suggested fix
- static importJSON(serializedNode: SerializedTextNode) { - return TextNode.importJSON(serializedNode); + static importJSON(serializedNode: SerializedTextNode): ExtendedTextNode { + return new ExtendedTextNode(serializedNode.text).updateFromJSON(serializedNode); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/ExtendedTextNode.ts` around lines 37 - 39, The current importJSON delegates to TextNode.importJSON and returns a base TextNode, losing subclass identity; change ExtendedTextNode.importJSON to construct and return an ExtendedTextNode using the incoming SerializedTextNode payload (copy text, format/formatting/children/any custom extended properties) instead of calling TextNode.importJSON, and ensure the returned object has type 'extended-text' to match exportJSON; reference ExtendedTextNode.importJSON, ExtendedTextNode.exportJSON, and SerializedTextNode when implementing the conversion so all extended fields are restored during deserialization.packages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.ts (1)
40-42:⚠️ Potential issue | 🔴 CriticalReplace raw caption
innerHTMLassignment (XSS sink).Line 41 injects
node.captiondirectly into HTML. If this value is not sanitized at render time, it can execute script content.🔒 Safer assignment
- figcaption.innerHTML = node.caption; + figcaption.textContent = node.caption;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.ts` around lines 40 - 42, The code in codeblock-renderer.ts directly assigns node.caption to figcaption.innerHTML (XSS sink); change this to a safe assignment by using textContent (or a sanitizer) instead of innerHTML so arbitrary HTML/scripts in node.caption cannot execute. Locate the figcaption creation/assignment in the codeblock renderer (the block that creates figcaption and uses node.caption) and replace the innerHTML assignment with figcaption.textContent = node.caption or pass node.caption through your project's HTML sanitizer before setting innerHTML if rich HTML is intended.packages/kg-default-nodes/src/nodes/product/product-renderer.ts (1)
54-60:⚠️ Potential issue | 🔴 CriticalEscape the button label and sanitize the
hrefbefore templating.
cardTemplate/emailCardTemplatestill interpolatedata.productButtonanddata.productUrldirectly into the HTML string, and Line 59 materializes that string withinnerHTML. That leaves an XSS path for any product data that reaches these fields without prior escaping/URL validation. IfproductTitle/productDescriptionare intentionally sanitized HTML, keep those as-is, but the button text and link should not be emitted raw.Also applies to: 64-90, 93-196
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/product/product-renderer.ts` around lines 54 - 60, The cardTemplate/emailCardTemplate flow currently injects unescaped productButton and productUrl into htmlString which is then assigned via element.innerHTML, creating XSS risk; update the code that prepares templateData (used by cardTemplate and emailCardTemplate) to HTML-escape the productButton label and to sanitize/validate productUrl (e.g., allow only http/https and remove javascript: or data: schemes) before passing into templates, or alternatively stop embedding those values in the raw template string and instead set the button text via textContent and set the anchor href via a validated value on the created element after element.innerHTML is assigned; refer to templateData, productButton, productUrl, cardTemplate, emailCardTemplate and element.innerHTML to locate where to apply escaping/URL validation and where to set safe DOM properties.packages/kg-default-nodes/src/nodes/video/video-renderer.ts (1)
16-21:⚠️ Potential issue | 🟠 Major
postUrlis still optional on the email branch.When
renderVideoNode()takes thetarget === 'email'path,emailCardTemplate()interpolatesoptions.postUrlinto thehrefattributes on Line 129 and Line 153. BecauseVideoRenderOptionsstill makespostUrloptional, the type system still allowshref="undefined". This matches the earlier review comment and still looks unresolved.🔧 Suggested fix
-interface VideoRenderOptions { +interface BaseVideoRenderOptions { createDocument?: () => Document; dom?: { window: { document: Document } }; - target?: string; - postUrl?: string; [key: string]: unknown; } + +type VideoRenderOptions = + | (BaseVideoRenderOptions & {target: 'email'; postUrl: string}) + | (BaseVideoRenderOptions & {target?: string; postUrl?: string}); + +type EmailVideoRenderOptions = Extract<VideoRenderOptions, {target: 'email'}>; @@ -export function emailCardTemplate({node, options, cardClasses}: {node: VideoNodeData, options: VideoRenderOptions, cardClasses: string}) { +export function emailCardTemplate({node, options, cardClasses}: {node: VideoNodeData, options: EmailVideoRenderOptions, cardClasses: string}) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/video/video-renderer.ts` around lines 16 - 21, VideoRenderOptions currently declares postUrl as optional which allows renderVideoNode's email path (emailCardTemplate) to interpolate undefined into hrefs; make postUrl required for the email path by either (A) changing VideoRenderOptions.postUrl to mandatory (remove the ?) so callers always provide it, or (B) narrow types in renderVideoNode/emailCardTemplate to accept a new EmailVideoRenderOptions with postUrl: string and validate/throw if missing before building the email template; update function signatures (renderVideoNode and emailCardTemplate) to use the stricter type or add a runtime check that throws a clear error when target === 'email' and options.postUrl is undefined to prevent href="undefined".
🧹 Nitpick comments (14)
packages/kg-default-nodes/src/nodes/email-cta/email-cta-renderer.ts (1)
6-21: Consider consolidatingRenderOptionsinto a shared type.The
EmailCtaNodeDatainterface correctly types all the node properties used in this renderer. TheRenderOptionsinterface with its index signature is compatible withaddCreateDocumentOption's expectedRecord<string, unknown>parameter.However, similar
RenderOptionsinterfaces likely exist across multiple renderers (bookmark, email, file, audio, button, etc.). Consider extracting this to a shared type in a common location to reduce duplication and ensure consistency.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/email-cta/email-cta-renderer.ts` around lines 6 - 21, Extract the local RenderOptions interface into a shared type and replace the renderer-local declaration with an import: create a common types file (e.g., a shared renderer-types module) that exports the RenderOptions shape used across renderers, update EmailCtaNodeRenderer to import and use that shared RenderOptions type instead of its own declaration, and update other renderer files (bookmark, email, file, audio, button, etc.) to consume the same shared type so the signature matches the expected Record<string, unknown> for addCreateDocumentOption and avoids duplication.packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts (2)
129-132: Typo in variable name:audioDUrationNodeThe variable has an inconsistent capital
U- should beaudioDurationNodeto match the camelCase convention used elsewhere in this file (e.g.,audioDuration,audioDurationTotal).✏️ Suggested fix
- const audioDUrationNode = document.createElement('span'); - audioDUrationNode.setAttribute('class', 'kg-audio-duration'); - audioDUrationNode.textContent = String(node.duration); - audioDurationTotal.appendChild(audioDUrationNode); + const audioDurationNode = document.createElement('span'); + audioDurationNode.setAttribute('class', 'kg-audio-duration'); + audioDurationNode.textContent = String(node.duration); + audioDurationTotal.appendChild(audioDurationNode);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts` around lines 129 - 132, Rename the mistakenly cased variable audioDUrationNode to audioDurationNode to follow camelCase and match surrounding identifiers; update all occurrences in audio-renderer.ts where audioDUrationNode is declared and referenced (e.g., the const declaration, setAttribute, textContent assignment, and appendChild call) so they consistently use audioDurationNode.
184-235: Pre-existing consideration: HTML escaping in email template.The email template interpolates
node.titledirectly into HTML (line 205) without escaping. Iftitlecontains HTML special characters or malicious content, it could cause rendering issues or XSS in email clients that execute JavaScript.This appears to be pre-existing behavior from before the TypeScript migration. If the input data is trusted/sanitized upstream, this may be acceptable—just flagging for awareness.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts` around lines 184 - 235, The emailTemplate function in audio-renderer.ts inserts node.title directly into the HTML string, risking broken rendering or XSS; fix it by escaping the title before interpolation (e.g. add or reuse an escapeHtml(text) helper and use escapeHtml(node.title) when building the html) or avoid string interpolation for the title by creating the title element via DOM APIs (createElement / createTextNode) and inserting the text node into the container after container.innerHTML is set; update the emailTemplate implementation (reference: function emailTemplate, symbol node.title) to ensure the title is safely escaped/inserted.packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts (2)
213-216:createDocument()is called twice on the email path; first call is unused.On the email branch, Line 215 creates a document that is never used, then Line 238 creates another one. This can introduce avoidable side effects and extra work.
Refactor to one call per execution path
export function renderHeaderNodeV2(dataset: HeaderV2DatasetNode, options: HeaderV2RenderOptions = {}) { addCreateDocumentOption(options); - const document = options.createDocument!(); + const createDocument = options.createDocument!(); const node = { // ... }; if (options.target === 'email') { - const emailDoc = options.createDocument!(); + const emailDoc = createDocument(); const emailDiv = emailDoc.createElement('div'); emailDiv.innerHTML = emailTemplate(node, options)?.trim(); return {element: emailDiv.firstElementChild}; } + const document = createDocument(); const htmlString = cardTemplate(node, options);Also applies to: 237-239
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts` around lines 213 - 216, The renderHeaderNodeV2 currently invokes options.createDocument() eagerly (after addCreateDocumentOption) and again later on the email branch, causing an unused/duplicate call; update renderHeaderNodeV2 to only call options.createDocument() once per control-flow path by removing the initial unused call at the top and invoking options.createDocument() lazily inside each branch that actually needs a Document (e.g., the email branch where the later call currently exists), or reuse a single local variable if both branches need the same Document; keep references to addCreateDocumentOption and options.createDocument to locate and fix the logic.
71-71: Replace the broad type assertion with a structurally compatible approach.The cast
options as Parameters<typeof getSrcsetAttribute>[0]['options']masks structural incompatibility betweenHeaderV2RenderOptionsandImageRenderOptions.HeaderV2RenderOptionslacks the required fields (siteUrl,canTransformImage,imageOptimization) and relies on its index signature[key: string]: unknownto permit the cast. This pattern can hide future type drift.Extract a typed subset from
optionsthat matchesImageRenderOptionsshape, or alignHeaderV2RenderOptionsto include the expected fields.Suggested approach
-const srcsetValue = getSrcsetAttribute({...bgImage, options: options as Parameters<typeof getSrcsetAttribute>[0]['options']}); +type SrcsetOptions = Parameters<typeof getSrcsetAttribute>[0]['options']; +const srcsetValue = getSrcsetAttribute({ + ...bgImage, + options: { + siteUrl: options.siteUrl, + canTransformImage: options.canTransformImage, + imageOptimization: options.imageOptimization + } as SrcsetOptions +});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts` at line 71, The current broad cast of `options as Parameters<typeof getSrcsetAttribute>[0]['options']` hides incompatibilities between `HeaderV2RenderOptions` and `ImageRenderOptions`; instead, create a properly typed subset object with the fields `siteUrl`, `canTransformImage`, and `imageOptimization` (and any other properties required by `ImageRenderOptions`) sourced from `options`, then pass that subset into `getSrcsetAttribute` (i.e., construct `const imgOptions: ImageRenderOptions = { siteUrl: options.siteUrl, canTransformImage: options.canTransformImage, imageOptimization: options.imageOptimization, ... }` and call `getSrcsetAttribute({...bgImage, options: imgOptions})`), or alternatively extend/adjust `HeaderV2RenderOptions` to include the same properties so a direct pass is type-safe.packages/kg-default-nodes/src/nodes/image/image-parser.ts (2)
28-30: Remove unreachableif (!img)inside figure conversion.
imgis already validated before the conversion object is created, so this branch is dead code.Proposed diff
- if (!img) { - return null; - } - const payload = readImageAttributesFromElement(img);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/image/image-parser.ts` around lines 28 - 30, Remove the unreachable branch that checks if (!img) inside the figure conversion: since img is validated earlier, delete the entire conditional and its null return so the conversion logic flows without dead code; locate the block that constructs the figure conversion (the code referencing img inside the image->figure conversion in image-parser.ts) and remove the if (!img) { return null; } branch, leaving the rest of the conversion intact.
5-5: Tighten parser constructor typing to remove unsafe casts and unreachable code.The
parseImageNodefunction accepts constructors returningunknownand force-casts nodes toLexicalNode. Change the constructor parameter to returnLexicalNodedirectly, eliminating both casts. Additionally, remove the unreachable guard at lines 28-30 (theimgvariable is already checked at line 21 before entering the conversion function).This pattern appears across all parser functions in the codebase—apply broadly or consider a systematic refactor.
Proposed diff
-export function parseImageNode(ImageNode: new (data: Record<string, unknown>) => unknown) { +export function parseImageNode(ImageNode: new (data: Record<string, unknown>) => LexicalNode) { return { img: () => ({ conversion(domNode: HTMLElement) { if (domNode.tagName === 'IMG') { const {src, width, height, alt, title, href} = readImageAttributesFromElement(domNode as HTMLImageElement); const node = new ImageNode({alt, src, title, width, height, href}); - return {node: node as LexicalNode}; + return {node}; } return null; }, priority: 1 as const }), figure: (nodeElem: HTMLElement) => { const img = nodeElem.querySelector('img'); if (img) { return { conversion(domNode: HTMLElement) { const kgClass = domNode.className.match(/kg-width-(wide|full)/); const grafClass = domNode.className.match(/graf--layout(FillWidth|OutsetCenter)/); - if (!img) { - return null; - } - const payload = readImageAttributesFromElement(img);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/image/image-parser.ts` at line 5, Change the parseImageNode signature so the ImageNode constructor returns LexicalNode instead of unknown (i.e., ImageNode: new (data: Record<string, unknown>) => LexicalNode), then remove any force-casts of the created node to LexicalNode inside parseImageNode and adjust usages to use the correct type directly; also delete the redundant/unreachable guard that checks img again inside the conversion function (the img is already validated before calling the converter), and propagate the same tighter constructor typing and guard removal pattern to the other parser functions that follow this pattern (so you eliminate unsafe casts and unreachable code).packages/kg-default-nodes/src/nodes/codeblock/codeblock-parser.ts (1)
24-30: Deduplicate language extraction to avoid parser drift.The class-to-language parsing logic is duplicated in both conversion paths. Extracting it to a small helper keeps both paths behaviorally identical over time.
♻️ Refactor sketch
+function extractLanguageFromClasses(preClass: string, codeClass: string): string | undefined { + const langRegex = /lang(?:uage)?-(.*?)(?:\s|$)/i; + const match = preClass.match(langRegex) || codeClass.match(langRegex); + return match?.[1]?.toLowerCase(); +} ... - const preClass = pre.getAttribute('class') || ''; - const codeClass = code.getAttribute('class') || ''; - const langRegex = /lang(?:uage)?-(.*?)(?:\s|$)/i; - const languageMatches = preClass.match(langRegex) || codeClass.match(langRegex); - if (languageMatches) { - payload.language = languageMatches[1].toLowerCase(); - } + const language = extractLanguageFromClasses( + pre.getAttribute('class') || '', + code.getAttribute('class') || '' + ); + if (language) { + payload.language = language; + } ... - const preClass = domNode.getAttribute('class') || ''; - const codeClass = codeElement.getAttribute('class') || ''; - const langRegex = /lang(?:uage)?-(.*?)(?:\s|$)/i; - const languageMatches = preClass.match(langRegex) || codeClass.match(langRegex); - if (languageMatches) { - payload.language = languageMatches[1].toLowerCase(); - } + const language = extractLanguageFromClasses( + domNode.getAttribute('class') || '', + codeElement.getAttribute('class') || '' + ); + if (language) { + payload.language = language; + }Also applies to: 49-53
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/codeblock/codeblock-parser.ts` around lines 24 - 30, The language extraction logic in codeblock-parser.ts (the langRegex, preClass/codeClass matching and setting payload.language) is duplicated; extract it into a small helper (e.g., getLanguageFromNodes or extractLanguageFromClasses) that accepts the pre and code elements or their class strings, runs the existing regex match, returns the lowercased language (or null/undefined) and then use that helper in both places where preClass/codeClass are currently handled (the block that sets payload.language at the first occurrence and the second occurrence around lines 49-53) so both conversion paths share identical logic.packages/kg-default-nodes/test/nodes/file.test.ts (1)
67-69: Remove unnecessary double-cast and align test assertion withfileSizestring type.The double-cast
as unknown as stringon line 67 defeats TypeScript's strict typing. SincefileSizeis defined asstringin the interface (see FileNode.ts), the assertion on line 68 comparing against a numeric value is inconsistent. TheformattedFileSizegetter converts the string to a number internally, so the fix below maintains correctness while improving type safety:♻️ Proposed cleanup
- node.fileSize = 123456 as unknown as string; - node.fileSize.should.equal(123456); + node.fileSize = '123456'; + node.fileSize.should.equal('123456'); node.formattedFileSize.should.equal('121 KB');🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/file.test.ts` around lines 67 - 69, Remove the unnecessary "as unknown as string" double-cast and make the test set node.fileSize to a string (e.g., node.fileSize = '123456') to match the FileNode.ts type; update the assertion that currently checks node.fileSize.should.equal(123456) to assert the string value (e.g., node.fileSize.should.equal('123456')) while keeping the existing node.formattedFileSize.should.equal('121 KB') check unchanged so the formattedFileSize getter still converts the string to a number internally.packages/kg-default-nodes/src/nodes/embed/embed-parser.ts (1)
64-65: Simplify the type cast chain.The double cast
domNode as unknown as HTMLIFrameElementcan be simplified. Since theiframecheck already confirms this is anIFRAMEelement, consider typing the conversion callback parameter more specifically:- conversion(domNode: HTMLElement) { - const payload = _createPayloadForIframe(domNode as unknown as HTMLIFrameElement); + conversion(domNode: HTMLIFrameElement) { + const payload = _createPayloadForIframe(domNode);This matches the guard at line 62 that confirms
nodeElem.tagName === 'IFRAME'.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/embed/embed-parser.ts` around lines 64 - 65, The conversion callback currently types its parameter as HTMLElement and then does a double cast when calling _createPayloadForIframe; change the conversion parameter to a more specific HTMLIFrameElement (e.g., conversion(domNode: HTMLIFrameElement)) so the iframe guard (nodeElem.tagName === 'IFRAME') aligns with the parameter type and you can call _createPayloadForIframe(domNode) without the unnecessary "as unknown as" cast.packages/kg-default-nodes/test/nodes/email.test.ts (1)
158-159: Type cast pattern forexportDOMis repeated throughout tests.The
as unknown as LexicalEditorcast works around a type mismatch whereexportDOMexpectsLexicalEditorbut tests pass render options. This pattern appears consistently across all test files in this migration.Consider adding a test utility type or wrapper to encapsulate this cast, improving readability and centralizing the workaround:
// In test-utils/index.ts export type ExportDOMOptions = Record<string, unknown>; export function asEditorForExport(options: ExportDOMOptions): LexicalEditor { return options as unknown as LexicalEditor; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/email.test.ts` around lines 158 - 159, Tests repeatedly use the unsafe cast "as unknown as LexicalEditor" when calling exportDOM; centralize this workaround by adding a small test utility: define a type ExportDOMOptions and a helper function asEditorForExport(options) that returns options cast to LexicalEditor, then replace occurrences of "... as unknown as LexicalEditor" in tests (e.g., in emailNode.exportDOM calls) with a call to asEditorForExport(options) to improve readability and maintainability.packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts (2)
10-15: Consider centralizingRenderOptionsinterface.The
RenderOptionsinterface pattern (withcreateDocument,dom,target, and index signature) is duplicated across multiple renderers in this migration. This could be extracted to a shared type in a common location to reduce duplication and ensure consistency.Also applies to: 4-8
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts` around lines 10 - 15, Extract the duplicated RenderOptions interface (with createDocument, dom.window.document, target and the index signature) into a single exported/shared type (e.g., RenderOptions) in a common rendering types module, export it, then replace the local interface declarations in callout-renderer.ts and the other renderers with imports of that shared RenderOptions; ensure the shared type preserves createDocument?: () => Document, dom?: { window: { document: Document } }, target?: string and [key: string]: unknown so existing call sites (e.g., functions referencing createDocument, dom, or target) remain type-compatible.
25-27: Mutating input parameternode.backgroundColoris a side effect.The function modifies the incoming
nodeobject directly. If callers don't expect this mutation, it could cause subtle bugs. Consider working with a local copy:- if (!node.backgroundColor || !node.backgroundColor.match(/^[a-zA-Z\d-]+$/)) { - node.backgroundColor = 'white'; - } + let backgroundColor = node.backgroundColor; + if (!backgroundColor || !backgroundColor.match(/^[a-zA-Z\d-]+$/)) { + backgroundColor = 'white'; + }Then use
backgroundColorin the class assignment on line 29.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts` around lines 25 - 27, The code mutates the incoming node by assigning node.backgroundColor = 'white'; instead create a local const (e.g. const backgroundColor = node.backgroundColor && node.backgroundColor.match(/^[a-zA-Z\d-]+$/) ? node.backgroundColor : 'white') and use that local backgroundColor for the class assignment instead of mutating node.backgroundColor; update any references in this file that currently read node.backgroundColor (notably the className construction in the callout renderer) to use the new local variable.packages/kg-default-nodes/eslint.config.mjs (1)
23-29: Disablingno-unsafe-declaration-mergingwarrants documentation.The rule is disabled for all source files. If this is specifically needed for Lexical's decorator node patterns (interface + class merging), consider adding a comment explaining why, or narrowing the scope to specific files that require it.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/eslint.config.mjs` around lines 23 - 29, The rule '@typescript-eslint/no-unsafe-declaration-merging' is globally disabled in the ESLint config; either document why it's disabled (e.g., a brief comment stating it's required for Lexical's decorator node patterns where interface+class merging is intentional) or restrict the disablement to the specific files that need it (for example narrow the files pattern from 'src/**/*.ts' to the Lexical node files). Update the config entry for the rule in the object containing files: ['src/**/*.ts'] so it includes a concise comment referencing Lexical decorator node merging or change the files array to only target the module(s) that require declaration merging.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 60da2381-4d9e-4c8c-86fa-ee9bf596a84d
⛔ Files ignored due to path filters (2)
packages/kg-default-nodes/src/nodes/at-link/kg-link.svgis excluded by!**/*.svgyarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (162)
packages/kg-default-nodes/eslint.config.mjspackages/kg-default-nodes/index.jspackages/kg-default-nodes/lib/kg-default-nodes.jspackages/kg-default-nodes/lib/nodes/at-link/index.jspackages/kg-default-nodes/lib/nodes/codeblock/codeblock-parser.jspackages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-parser.jspackages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-renderer.jspackages/kg-default-nodes/lib/nodes/html/html-parser.jspackages/kg-default-nodes/lib/nodes/markdown/markdown-renderer.jspackages/kg-default-nodes/lib/nodes/paywall/paywall-parser.jspackages/kg-default-nodes/lib/utils/is-unsplash-image.jspackages/kg-default-nodes/package.jsonpackages/kg-default-nodes/rollup.config.mjspackages/kg-default-nodes/src/KoenigDecoratorNode.tspackages/kg-default-nodes/src/generate-decorator-node.tspackages/kg-default-nodes/src/index.tspackages/kg-default-nodes/src/kg-default-nodes.tspackages/kg-default-nodes/src/nodes/ExtendedHeadingNode.tspackages/kg-default-nodes/src/nodes/ExtendedQuoteNode.tspackages/kg-default-nodes/src/nodes/ExtendedTextNode.tspackages/kg-default-nodes/src/nodes/TKNode.tspackages/kg-default-nodes/src/nodes/aside/AsideNode.tspackages/kg-default-nodes/src/nodes/aside/AsideParser.tspackages/kg-default-nodes/src/nodes/at-link/AtLinkNode.tspackages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.tspackages/kg-default-nodes/src/nodes/at-link/index.tspackages/kg-default-nodes/src/nodes/audio/AudioNode.tspackages/kg-default-nodes/src/nodes/audio/audio-parser.tspackages/kg-default-nodes/src/nodes/audio/audio-renderer.tspackages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.tspackages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.tspackages/kg-default-nodes/src/nodes/bookmark/bookmark-renderer.tspackages/kg-default-nodes/src/nodes/button/ButtonNode.tspackages/kg-default-nodes/src/nodes/button/button-parser.tspackages/kg-default-nodes/src/nodes/button/button-renderer.tspackages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.tspackages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.tspackages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.tspackages/kg-default-nodes/src/nodes/callout/CalloutNode.tspackages/kg-default-nodes/src/nodes/callout/callout-parser.tspackages/kg-default-nodes/src/nodes/callout/callout-renderer.tspackages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.tspackages/kg-default-nodes/src/nodes/codeblock/codeblock-parser.tspackages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.tspackages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.tspackages/kg-default-nodes/src/nodes/email-cta/email-cta-renderer.tspackages/kg-default-nodes/src/nodes/email/EmailNode.tspackages/kg-default-nodes/src/nodes/email/email-renderer.tspackages/kg-default-nodes/src/nodes/embed/EmbedNode.tspackages/kg-default-nodes/src/nodes/embed/embed-parser.tspackages/kg-default-nodes/src/nodes/embed/embed-renderer.tspackages/kg-default-nodes/src/nodes/embed/types/twitter.tspackages/kg-default-nodes/src/nodes/file/FileNode.tspackages/kg-default-nodes/src/nodes/file/file-parser.tspackages/kg-default-nodes/src/nodes/file/file-renderer.tspackages/kg-default-nodes/src/nodes/gallery/GalleryNode.tspackages/kg-default-nodes/src/nodes/gallery/gallery-parser.tspackages/kg-default-nodes/src/nodes/gallery/gallery-renderer.tspackages/kg-default-nodes/src/nodes/header/HeaderNode.tspackages/kg-default-nodes/src/nodes/header/parsers/header-parser.tspackages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.tspackages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.tspackages/kg-default-nodes/src/nodes/horizontalrule/HorizontalRuleNode.tspackages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-parser.tspackages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-renderer.tspackages/kg-default-nodes/src/nodes/html/HtmlNode.tspackages/kg-default-nodes/src/nodes/html/html-parser.tspackages/kg-default-nodes/src/nodes/html/html-renderer.tspackages/kg-default-nodes/src/nodes/image/ImageNode.tspackages/kg-default-nodes/src/nodes/image/image-parser.tspackages/kg-default-nodes/src/nodes/image/image-renderer.tspackages/kg-default-nodes/src/nodes/markdown/MarkdownNode.tspackages/kg-default-nodes/src/nodes/markdown/markdown-renderer.tspackages/kg-default-nodes/src/nodes/paywall/PaywallNode.tspackages/kg-default-nodes/src/nodes/paywall/paywall-parser.tspackages/kg-default-nodes/src/nodes/paywall/paywall-renderer.tspackages/kg-default-nodes/src/nodes/product/ProductNode.tspackages/kg-default-nodes/src/nodes/product/product-parser.tspackages/kg-default-nodes/src/nodes/product/product-renderer.tspackages/kg-default-nodes/src/nodes/signup/SignupNode.tspackages/kg-default-nodes/src/nodes/signup/signup-parser.tspackages/kg-default-nodes/src/nodes/signup/signup-renderer.tspackages/kg-default-nodes/src/nodes/toggle/ToggleNode.tspackages/kg-default-nodes/src/nodes/toggle/toggle-parser.tspackages/kg-default-nodes/src/nodes/toggle/toggle-renderer.tspackages/kg-default-nodes/src/nodes/transistor/TransistorNode.tspackages/kg-default-nodes/src/nodes/transistor/transistor-renderer.tspackages/kg-default-nodes/src/nodes/video/VideoNode.tspackages/kg-default-nodes/src/nodes/video/video-parser.tspackages/kg-default-nodes/src/nodes/video/video-renderer.tspackages/kg-default-nodes/src/nodes/zwnj/ZWNJNode.tspackages/kg-default-nodes/src/serializers/linebreak.tspackages/kg-default-nodes/src/serializers/paragraph.tspackages/kg-default-nodes/src/svg.d.tspackages/kg-default-nodes/src/utils/add-create-document-option.tspackages/kg-default-nodes/src/utils/build-clean-basic-html-for-element.tspackages/kg-default-nodes/src/utils/clean-dom.tspackages/kg-default-nodes/src/utils/escape-html.tspackages/kg-default-nodes/src/utils/get-available-image-widths.tspackages/kg-default-nodes/src/utils/get-resized-image-dimensions.tspackages/kg-default-nodes/src/utils/is-local-content-image.tspackages/kg-default-nodes/src/utils/is-unsplash-image.tspackages/kg-default-nodes/src/utils/read-caption-from-element.tspackages/kg-default-nodes/src/utils/read-image-attributes-from-element.tspackages/kg-default-nodes/src/utils/read-text-content.tspackages/kg-default-nodes/src/utils/render-empty-container.tspackages/kg-default-nodes/src/utils/render-helpers/email-button.tspackages/kg-default-nodes/src/utils/replacement-strings.tspackages/kg-default-nodes/src/utils/rgb-to-hex.tspackages/kg-default-nodes/src/utils/set-src-background-from-parent.tspackages/kg-default-nodes/src/utils/size-byte-converter.tspackages/kg-default-nodes/src/utils/slugify.tspackages/kg-default-nodes/src/utils/srcset-attribute.tspackages/kg-default-nodes/src/utils/tagged-template-fns.tspackages/kg-default-nodes/src/utils/truncate.tspackages/kg-default-nodes/src/utils/visibility.tspackages/kg-default-nodes/test/generate-decorator-node.test.tspackages/kg-default-nodes/test/nodes/aside.test.tspackages/kg-default-nodes/test/nodes/at-link-search.test.tspackages/kg-default-nodes/test/nodes/at-link.test.tspackages/kg-default-nodes/test/nodes/audio.test.tspackages/kg-default-nodes/test/nodes/bookmark.test.tspackages/kg-default-nodes/test/nodes/button.test.tspackages/kg-default-nodes/test/nodes/call-to-action.test.tspackages/kg-default-nodes/test/nodes/callout.test.tspackages/kg-default-nodes/test/nodes/codeblock.test.tspackages/kg-default-nodes/test/nodes/email-cta.test.tspackages/kg-default-nodes/test/nodes/email.test.tspackages/kg-default-nodes/test/nodes/embed.test.tspackages/kg-default-nodes/test/nodes/file.test.tspackages/kg-default-nodes/test/nodes/gallery.test.tspackages/kg-default-nodes/test/nodes/header.test.tspackages/kg-default-nodes/test/nodes/horizontalrule.test.tspackages/kg-default-nodes/test/nodes/html.test.tspackages/kg-default-nodes/test/nodes/image.test.tspackages/kg-default-nodes/test/nodes/markdown.test.tspackages/kg-default-nodes/test/nodes/paywall.test.tspackages/kg-default-nodes/test/nodes/product.test.tspackages/kg-default-nodes/test/nodes/signup.test.tspackages/kg-default-nodes/test/nodes/tk.test.tspackages/kg-default-nodes/test/nodes/toggle.test.tspackages/kg-default-nodes/test/nodes/transistor.test.tspackages/kg-default-nodes/test/nodes/video.test.tspackages/kg-default-nodes/test/nodes/zwnj.test.tspackages/kg-default-nodes/test/serializers/linebreak.test.tspackages/kg-default-nodes/test/serializers/paragraph.test.tspackages/kg-default-nodes/test/test-utils/assertions.jspackages/kg-default-nodes/test/test-utils/assertions.tspackages/kg-default-nodes/test/test-utils/html-minifier.d.tspackages/kg-default-nodes/test/test-utils/index.jspackages/kg-default-nodes/test/test-utils/index.tspackages/kg-default-nodes/test/test-utils/overrides.jspackages/kg-default-nodes/test/test-utils/overrides.tspackages/kg-default-nodes/test/test-utils/should-assertions.d.tspackages/kg-default-nodes/test/test-utils/should.d.tspackages/kg-default-nodes/test/utils/rgb-to-hex.test.tspackages/kg-default-nodes/test/utils/tagged-template-fns.test.tspackages/kg-default-nodes/test/utils/visibility.test.tspackages/kg-default-nodes/tsconfig.cjs.jsonpackages/kg-default-nodes/tsconfig.jsonpackages/kg-default-nodes/tsconfig.test.jsonpackages/koenig-lexical/test/unit/hooks/useVisibilityToggle.test.js
💤 Files with no reviewable changes (14)
- packages/kg-default-nodes/index.js
- packages/kg-default-nodes/lib/nodes/paywall/paywall-parser.js
- packages/kg-default-nodes/lib/utils/is-unsplash-image.js
- packages/kg-default-nodes/lib/nodes/at-link/index.js
- packages/kg-default-nodes/lib/nodes/codeblock/codeblock-parser.js
- packages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-parser.js
- packages/kg-default-nodes/lib/nodes/horizontalrule/horizontalrule-renderer.js
- packages/kg-default-nodes/lib/nodes/html/html-parser.js
- packages/kg-default-nodes/lib/nodes/markdown/markdown-renderer.js
- packages/kg-default-nodes/rollup.config.mjs
- packages/kg-default-nodes/test/test-utils/assertions.js
- packages/kg-default-nodes/test/test-utils/index.js
- packages/kg-default-nodes/test/test-utils/overrides.js
- packages/kg-default-nodes/lib/kg-default-nodes.js
✅ Files skipped from review due to trivial changes (33)
- packages/kg-default-nodes/src/index.ts
- packages/kg-default-nodes/src/serializers/paragraph.ts
- packages/kg-default-nodes/src/utils/escape-html.ts
- packages/kg-default-nodes/src/svg.d.ts
- packages/kg-default-nodes/src/KoenigDecoratorNode.ts
- packages/kg-default-nodes/src/nodes/at-link/index.ts
- packages/kg-default-nodes/src/nodes/audio/AudioNode.ts
- packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-parser.ts
- packages/kg-default-nodes/src/utils/clean-dom.ts
- packages/kg-default-nodes/test/test-utils/overrides.ts
- packages/kg-default-nodes/tsconfig.test.json
- packages/kg-default-nodes/test/test-utils/html-minifier.d.ts
- packages/kg-default-nodes/tsconfig.cjs.json
- packages/kg-default-nodes/test/utils/tagged-template-fns.test.ts
- packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-renderer.ts
- packages/kg-default-nodes/src/nodes/signup/signup-renderer.ts
- packages/kg-default-nodes/src/utils/is-local-content-image.ts
- packages/kg-default-nodes/src/utils/is-unsplash-image.ts
- packages/kg-default-nodes/src/utils/slugify.ts
- packages/kg-default-nodes/src/utils/render-empty-container.ts
- packages/kg-default-nodes/src/utils/render-helpers/email-button.ts
- packages/kg-default-nodes/test/nodes/image.test.ts
- packages/kg-default-nodes/test/test-utils/should.d.ts
- packages/kg-default-nodes/tsconfig.json
- packages/kg-default-nodes/test/nodes/transistor.test.ts
- packages/kg-default-nodes/test/nodes/tk.test.ts
- packages/kg-default-nodes/test/test-utils/assertions.ts
- packages/kg-default-nodes/test/nodes/video.test.ts
- packages/kg-default-nodes/src/utils/tagged-template-fns.ts
- packages/kg-default-nodes/src/utils/visibility.ts
- packages/kg-default-nodes/src/utils/build-clean-basic-html-for-element.ts
- packages/kg-default-nodes/test/test-utils/should-assertions.d.ts
- packages/kg-default-nodes/src/nodes/paywall/paywall-parser.ts
🚧 Files skipped from review as they are similar to previous changes (72)
- packages/kg-default-nodes/src/nodes/horizontalrule/HorizontalRuleNode.ts
- packages/kg-default-nodes/src/serializers/linebreak.ts
- packages/kg-default-nodes/src/nodes/ExtendedQuoteNode.ts
- packages/kg-default-nodes/src/nodes/TKNode.ts
- packages/kg-default-nodes/src/nodes/aside/AsideParser.ts
- packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts
- packages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.ts
- packages/kg-default-nodes/src/nodes/bookmark/bookmark-renderer.ts
- packages/kg-default-nodes/src/nodes/button/ButtonNode.ts
- packages/kg-default-nodes/src/nodes/button/button-renderer.ts
- packages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.ts
- packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts
- packages/kg-default-nodes/src/nodes/callout/callout-parser.ts
- packages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.ts
- packages/kg-default-nodes/src/nodes/email/EmailNode.ts
- packages/kg-default-nodes/src/nodes/email/email-renderer.ts
- packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts
- packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts
- packages/kg-default-nodes/src/nodes/file/FileNode.ts
- packages/kg-default-nodes/src/nodes/file/file-renderer.ts
- packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts
- packages/kg-default-nodes/src/nodes/header/HeaderNode.ts
- packages/kg-default-nodes/src/nodes/header/parsers/header-parser.ts
- packages/kg-default-nodes/src/nodes/html/HtmlNode.ts
- packages/kg-default-nodes/src/nodes/image/ImageNode.ts
- packages/kg-default-nodes/test/nodes/audio.test.ts
- packages/kg-default-nodes/test/nodes/button.test.ts
- packages/kg-default-nodes/test/nodes/horizontalrule.test.ts
- packages/kg-default-nodes/test/nodes/toggle.test.ts
- packages/kg-default-nodes/test/nodes/zwnj.test.ts
- packages/kg-default-nodes/test/nodes/paywall.test.ts
- packages/kg-default-nodes/test/nodes/markdown.test.ts
- packages/kg-default-nodes/src/nodes/html/html-parser.ts
- packages/kg-default-nodes/src/nodes/paywall/PaywallNode.ts
- packages/koenig-lexical/test/unit/hooks/useVisibilityToggle.test.js
- packages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.ts
- packages/kg-default-nodes/src/nodes/html/html-renderer.ts
- packages/kg-default-nodes/src/nodes/toggle/ToggleNode.ts
- packages/kg-default-nodes/src/nodes/transistor/TransistorNode.ts
- packages/kg-default-nodes/src/nodes/paywall/paywall-renderer.ts
- packages/kg-default-nodes/src/nodes/video/VideoNode.ts
- packages/kg-default-nodes/package.json
- packages/kg-default-nodes/src/kg-default-nodes.ts
- packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts
- packages/kg-default-nodes/src/nodes/audio/audio-parser.ts
- packages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.ts
- packages/kg-default-nodes/test/nodes/bookmark.test.ts
- packages/kg-default-nodes/test/nodes/codeblock.test.ts
- packages/kg-default-nodes/test/nodes/email-cta.test.ts
- packages/kg-default-nodes/test/nodes/header.test.ts
- packages/kg-default-nodes/test/utils/rgb-to-hex.test.ts
- packages/kg-default-nodes/test/serializers/paragraph.test.ts
- packages/kg-default-nodes/test/nodes/product.test.ts
- packages/kg-default-nodes/test/serializers/linebreak.test.ts
- packages/kg-default-nodes/test/nodes/signup.test.ts
- packages/kg-default-nodes/src/nodes/image/image-renderer.ts
- packages/kg-default-nodes/src/nodes/product/ProductNode.ts
- packages/kg-default-nodes/src/utils/get-resized-image-dimensions.ts
- packages/kg-default-nodes/src/utils/read-caption-from-element.ts
- packages/kg-default-nodes/src/utils/replacement-strings.ts
- packages/kg-default-nodes/src/utils/read-text-content.ts
- packages/kg-default-nodes/src/utils/read-image-attributes-from-element.ts
- packages/kg-default-nodes/src/nodes/button/button-parser.ts
- packages/kg-default-nodes/src/utils/rgb-to-hex.ts
- packages/kg-default-nodes/src/utils/size-byte-converter.ts
- packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts
- packages/kg-default-nodes/src/utils/truncate.ts
- packages/kg-default-nodes/src/utils/srcset-attribute.ts
- packages/kg-default-nodes/test/nodes/aside.test.ts
- packages/kg-default-nodes/src/utils/get-available-image-widths.ts
- packages/kg-default-nodes/src/utils/add-create-document-option.ts
- packages/kg-default-nodes/src/nodes/embed/types/twitter.ts
06e801f to
e38ff89
Compare
09cfc78 to
ad10088
Compare
6985fcd to
f995f5e
Compare
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts (1)
184-217:⚠️ Potential issue | 🟠 MajorGuard email rendering when
postUrlis missing.
RenderOptions.postUrlis optional, but this template unconditionally injects it intohref. Iftargetis email withoutpostUrl, output contains brokenhref="undefined"links.Proposed fix
export function renderAudioNode(node: AudioNodeData, options: RenderOptions = {}) { addCreateDocumentOption(options); const document = options.createDocument!(); if (!node.src || node.src.trim() === '') { return renderEmptyContainer(document); } const thumbnailCls = getThumbnailCls(node); const emptyThumbnailCls = getEmptyThumbnailCls(node); if (options.target === 'email') { - return emailTemplate(node, document, options, thumbnailCls, emptyThumbnailCls); + if (!options.postUrl) { + return renderEmptyContainer(document); + } + return emailTemplate(node, document, options.postUrl, thumbnailCls, emptyThumbnailCls); } else { return frontendTemplate(node, document, thumbnailCls, emptyThumbnailCls); } } -function emailTemplate(node: AudioNodeData, document: Document, options: RenderOptions, thumbnailCls: string, emptyThumbnailCls: string) { +function emailTemplate(node: AudioNodeData, document: Document, postUrl: string, thumbnailCls: string, emptyThumbnailCls: string) { const html = (` <table cellspacing="0" cellpadding="0" border="0" class="kg-audio-card"> <tr> <td> <table cellspacing="0" cellpadding="0" border="0" width="100%"> <tr> <td width="60"> - <a href="${options.postUrl}" style="display: block; width: 60px; height: 60px; padding-top: 4px; padding-right: 16px; padding-bottom: 4px; padding-left: 4px; border-radius: 2px;"> + <a href="${postUrl}" style="display: block; width: 60px; height: 60px; padding-top: 4px; padding-right: 16px; padding-bottom: 4px; padding-left: 4px; border-radius: 2px;"> ... - <a href="${options.postUrl}" style="position: absolute; display: block; top: 0; right: 0; bottom: 0; left: 0;"></a> + <a href="${postUrl}" style="position: absolute; display: block; top: 0; right: 0; bottom: 0; left: 0;"></a> ... - <a href="${options.postUrl}" class="kg-audio-title">${node.title}</a> + <a href="${postUrl}" class="kg-audio-title">${node.title}</a> ... - <a href="${options.postUrl}" class="kg-audio-play-button"></a> + <a href="${postUrl}" class="kg-audio-play-button"></a> ... - <a href="${options.postUrl}" class="kg-audio-duration">${getFormattedDuration(node.duration)}<span class="kg-audio-link"> • Click to play audio</span></a> + <a href="${postUrl}" class="kg-audio-duration">${getFormattedDuration(node.duration)}<span class="kg-audio-link"> • Click to play audio</span></a>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts` around lines 184 - 217, The emailTemplate function injects options.postUrl into multiple anchor hrefs regardless of presence, causing href="undefined"; update emailTemplate to guard all uses of RenderOptions.postUrl (e.g. the anchors around the thumbnail, title, play button and duration) by either conditionally omitting the href attribute when postUrl is falsy or substituting a safe fallback (e.g. '#' or empty string) so anchors are not rendered with "undefined"; check occurrences in emailTemplate (thumbnail anchor, title anchor, play-button anchor, duration anchor) and use a single pattern like postUrl ? `href="${options.postUrl}"` : '' to keep markup valid while preserving classes and getFormattedDuration(node.duration) usage.packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts (1)
27-31:⚠️ Potential issue | 🟡 MinorType for
imageUrlallowsnumberwhich seems incorrect.The
imageDatatype declaresimageUrl: string | number, but image URLs should always be strings. ThereadImageAttributesFromElementfunction returnssrcas a string, and line 35 assigns it directly. The| numberappears to be a copy-paste from the dimension fields.Suggested fix
- const imageData: {imageUrl: string | number; imageWidth: string | number | null; imageHeight: string | number | null} = { + const imageData: {imageUrl: string; imageWidth: string | number | null; imageHeight: string | number | null} = {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts` around lines 27 - 31, The imageData type incorrectly allows imageUrl to be a number; update the declaration so imageUrl is string only (remove | number) to match readImageAttributesFromElement which returns a string and the assignment at imageData.imageUrl; adjust the type signature where imageData is defined and any related uses to ensure imageUrl is typed as string while leaving imageWidth and imageHeight as string | number | null.
♻️ Duplicate comments (6)
packages/kg-default-nodes/src/nodes/file/file-renderer.ts (1)
55-68:⚠️ Potential issue | 🟠 MajorAvoid emitting empty email
hrefvalues whenpostUrlis unset.
Line 55,Line 60,Line 64, andLine 68currently fall back to'', which renders broken clickable links in email output. Useoptions.postUrl || node.src(the latter is already guaranteed byLine 27).Suggested fix
function emailTemplate(node: FileNodeData, document: Document, options: RenderOptions) { + const href = escapeHtml(options.postUrl || node.src); let iconCls; @@ - <a href="${escapeHtml(options.postUrl || '')}" class="kg-file-title">${escapeHtml(node.fileTitle)}</a> + <a href="${href}" class="kg-file-title">${escapeHtml(node.fileTitle)}</a> @@ - <a href="${escapeHtml(options.postUrl || '')}" class="kg-file-description">${escapeHtml(node.fileCaption)}</a> + <a href="${href}" class="kg-file-description">${escapeHtml(node.fileCaption)}</a> @@ - <a href="${escapeHtml(options.postUrl || '')}" class="kg-file-meta"><span class="kg-file-name">${escapeHtml(node.fileName)}</span> • ${bytesToSize(node.fileSize)}</a> + <a href="${href}" class="kg-file-meta"><span class="kg-file-name">${escapeHtml(node.fileName)}</span> • ${bytesToSize(node.fileSize)}</a> @@ - <a href="${escapeHtml(options.postUrl || '')}" style="display: block; top: 0; right: 0; bottom: 0; left: 0;"> + <a href="${href}" style="display: block; top: 0; right: 0; bottom: 0; left: 0;">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/file/file-renderer.ts` around lines 55 - 68, The template emits empty href attributes when options.postUrl is unset; update all href uses that currently use escapeHtml(options.postUrl || '') to fall back to node.src instead (use escapeHtml(options.postUrl || node.src)) so links for file title, caption, meta and thumbnail point to node.src when postUrl is missing; locate these occurrences in file-renderer.ts where hrefs wrap node.fileTitle, node.fileCaption, node.fileName and the thumbnail anchor and change the fallback accordingly.packages/kg-default-nodes/src/nodes/product/product-renderer.ts (1)
64-90:⚠️ Potential issue | 🔴 CriticalEscape interpolated product fields before HTML generation (XSS risk).
The templates still interpolate raw
datavalues into HTML strings and those strings are inserted viainnerHTML(Line 59). This re-opens an injection path for text and attribute fields (productTitle,productDescription,productButton,productUrl, and similarlyproductImageSrc).🔒 Suggested hardening
+import {escapeHtml} from '../../utils/escape-html.js'; import {addCreateDocumentOption} from '../../utils/add-create-document-option.js'; import {renderEmptyContainer} from '../../utils/render-empty-container.js'; import {getResizedImageDimensions} from '../../utils/get-resized-image-dimensions.js'; export function cardTemplate({data}: {data: Record<string, unknown>}) { + const productImageSrc = escapeHtml(String(data.productImageSrc ?? '')); + const productTitle = escapeHtml(String(data.productTitle ?? '')); + const productDescription = escapeHtml(String(data.productDescription ?? '')); + const productUrl = escapeHtml(String(data.productUrl ?? '')); + const productButton = escapeHtml(String(data.productButton ?? '')); + return ( ` <div class="kg-card kg-product-card"> <div class="kg-product-card-container"> - ${data.productImageSrc ? `<img src="${data.productImageSrc}" ${data.productImageWidth ? `width="${data.productImageWidth}"` : ''} ${data.productImageHeight ? `height="${data.productImageHeight}"` : ''} class="kg-product-card-image" loading="lazy" />` : ''} + ${data.productImageSrc ? `<img src="${productImageSrc}" ${data.productImageWidth ? `width="${data.productImageWidth}"` : ''} ${data.productImageHeight ? `height="${data.productImageHeight}"` : ''} class="kg-product-card-image" loading="lazy" />` : ''} <div class="kg-product-card-title-container"> - <h4 class="kg-product-card-title">${data.productTitle}</h4> + <h4 class="kg-product-card-title">${productTitle}</h4> </div> ... - <div class="kg-product-card-description">${data.productDescription}</div> + <div class="kg-product-card-description">${productDescription}</div> ${data.productButtonEnabled ? ` - <a href="${data.productUrl}" class="kg-product-card-button kg-product-card-btn-accent" target="_blank" rel="noopener noreferrer"><span>${data.productButton}</span></a> + <a href="${productUrl}" class="kg-product-card-button kg-product-card-btn-accent" target="_blank" rel="noopener noreferrer"><span>${productButton}</span></a> ` : ''} </div> </div> ` ); }Also applies to: 93-194
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/product/product-renderer.ts` around lines 64 - 90, The cardTemplate function is inserting raw data into an HTML string which is then used with innerHTML, creating an XSS risk; update cardTemplate to sanitize/escape all interpolated values (productTitle, productDescription, productButton, productUrl, productImageSrc, productImageWidth, productImageHeight and any starIcon/star class strings) before building the markup or, better, switch to constructing DOM nodes programmatically and set textContent/attribute values rather than embedding raw values in a template string; add or reuse a single escape/sanitize helper (e.g., escapeHtml or sanitizeUrl) and apply it to attribute and text interpolations used by cardTemplate (and the other template blocks referenced in the comment).packages/kg-default-nodes/test/nodes/gallery.test.ts (1)
627-629:⚠️ Potential issue | 🟡 MinorAvoid indexing
nodes[0]when empty results are valid.This assertion can fail for the wrong reason when parsing returns
[]. Prefer checking that no returned node is a gallery.Suggested fix
- nodes.length.should.not.equal(1); - nodes[0].getType().should.not.equal('gallery'); + nodes.some(node => node.getType() === 'gallery').should.be.false();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/gallery.test.ts` around lines 627 - 629, The test currently indexes nodes[0] which can throw when $generateNodesFromDOM(editor, document) returns an empty array; instead assert that no returned node is a gallery by replacing the length/index checks with a check on the nodes array (e.g., using Array.prototype.every or Array.prototype.some) to ensure every GalleryNode from $generateNodesFromDOM has getType() !== 'gallery' — update the assertions around the nodes variable and references to GalleryNode and getType() accordingly.packages/kg-default-nodes/package.json (1)
19-21:⚠️ Potential issue | 🟠 MajorClean
build/before emitting artifacts.
tscwill not remove outputs for renamed or deleted source files. Since this package now publishes the wholebuilddirectory, stale JS or.d.tsfiles can survive local rebuilds and get shipped unlessbuild/is cleaned first.Suggested script update
"scripts": { + "clean": "node -e \"require('node:fs').rmSync('build', {recursive: true, force: true})\"", "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", + "build": "yarn clean && tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"module\"}' > build/esm/package.json", + "prepare": "yarn build", + "pretest": "yarn build && tsc -p tsconfig.test.json",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/package.json` around lines 19 - 21, The build, prepare, and pretest npm scripts ("build", "prepare", "pretest") do not clean the build/ directory first, which allows stale artifacts to be published; update each script to remove or clean build/ before running tsc (e.g., run a cleanup command such as rimraf build or rm -rf build or call an existing "clean" npm script) so that build/ is freshly created prior to the existing tsc && tsc -p ... && echo ... sequence.packages/kg-default-nodes/test/nodes/header.test.ts (2)
151-159:⚠️ Potential issue | 🟡 MinorReplace this no-op with a real assertion.
void elementmakes the test pass regardless of whatexportDOM()returns, so the empty-render case is no longer validated.packages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.ts:23-78currently returns an element, notnull, so this should assert the current empty-container behavior explicitly instead of silently skipping it.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/header.test.ts` around lines 151 - 159, Replace the no-op "void element" with a real assertion that verifies the renderer's current empty-container behavior: call $createHeaderNode(...) and node.exportDOM(...), then assert that the returned element is a DOM Element (not null) and that it contains no meaningful content (e.g., has no child elements and empty/textContent trimmed is empty) to explicitly validate the behavior of the header renderer in packages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.ts and the exportDOM path.
468-476:⚠️ Potential issue | 🟡 MinorThis v2 "renders nothing" test also stopped asserting behavior.
Same issue here:
void elementturns the test into a no-op.packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts:213-266still returns anelementvalue, so please assert the actual result for the empty-content case rather than leaving it unverified.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/header.test.ts` around lines 468 - 476, Test currently discards the exportDOM result with "void element" making the case a no-op; instead, update the spec using $createHeaderNode and node.exportDOM(exportOptions) to assert the actual empty-content behavior produced by the v2 header renderer (the code path in the header renderer that handles header/subheader null + buttonEnabled=false). Replace the no-op with an assertion that matches the renderer's real return for empty content (for example assert that element is a DocumentFragment or Element with no children and no textContent / innerHTML), or explicitly assert the expected element type and that its childNodes.length === 0.
🧹 Nitpick comments (12)
packages/kg-default-nodes/src/nodes/embed/embed-parser.ts (1)
90-92: Minor: mutating the inputiframe.srcdirectly.This modifies the original DOM element's
srcattribute during parsing. If the parser ever runs on a live document (rather than a cloned/temporary one), this could cause unintended side effects. Consider storing the normalized URL in a local variable instead:♻️ Optional refactor
// if it's a schemaless URL, convert to https + let src = iframe.src; if (iframe.src.match(/^\/\//)) { - iframe.src = `https:${iframe.src}`; + src = `https:${iframe.src}`; } const payload: Record<string, unknown> = { - url: iframe.src + url: src }; - payload.html = iframe.outerHTML; + // Note: outerHTML still reflects original src; if normalized URL is needed in html, + // additional handling would be required + payload.html = iframe.outerHTML;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/embed/embed-parser.ts` around lines 90 - 92, The parser currently mutates the input DOM by assigning to iframe.src when normalizing protocol-relative URLs; instead, read iframe.src into a local variable (e.g., normalizedSrc), normalize that variable (if it matches /^\/\// prefix, prepend "https:"), and use the local normalizedSrc for any further processing or returned values rather than writing back to iframe.src in embed-parser.ts (look for the iframe.src handling block).packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts (1)
22-25: Consider extracting the complexcreateElementreturn type into a separate interface.The inline return type for
createElementis quite dense and spans multiple nested properties. Extracting it would improve readability.♻️ Suggested refactor
+interface BrowserCanvasContext { + fillStyle: string; + fillRect(x: number, y: number, w: number, h: number): void; + getImageData(x: number, y: number, w: number, h: number): { data: number[] }; +} + +interface BrowserCanvasElement extends BrowserElement { + innerHTML: string; + width: number; + height: number; + getContext(id: string): BrowserCanvasContext; +} + interface BrowserDocument { currentScript: { parentElement: BrowserElement } | null; - createElement(tag: string): BrowserElement & { innerHTML: string; width: number; height: number; getContext(id: string): { fillStyle: string; fillRect(x: number, y: number, w: number, h: number): void; getImageData(x: number, y: number, w: number, h: number): { data: number[] } } }; + createElement(tag: string): BrowserCanvasElement; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts` around lines 22 - 25, The inline complex return type of BrowserDocument.createElement should be extracted into a named interface to improve readability and reuse: define a new interface (e.g., CanvasElement or CreateElementReturn) that extends BrowserElement and declares innerHTML, width, height, getContext(...) returning the typed context and getImageData(...) returning the data array, then replace the inline return type in the BrowserDocument interface with this new interface; update any other uses of the same inline shape (if present) to reference the new interface and keep names BrowserDocument and createElement unchanged.packages/kg-default-nodes/src/nodes/html/html-parser.ts (1)
3-3: Tighten constructor typing to remove unsafeunknown -> LexicalNodecastsLine 3 currently allows non-Lexical constructors and defers failure to runtime. Prefer a generic constructor constrained to
LexicalNodeso Line 26 and Line 40 can returnnodedirectly.#!/bin/bash # Verify parser usage and HtmlNode inheritance before applying the generic type refactor. cat -n packages/kg-default-nodes/src/nodes/html/html-parser.ts cat -n packages/kg-default-nodes/src/nodes/html/HtmlNode.ts rg -n "parseHtmlNode\\(" packages/kg-default-nodes/src packages/kg-default-nodes/testAlso applies to: 25-26, 39-40
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/html/html-parser.ts` at line 3, The parseHtmlNode function currently accepts a constructor typed as new (data: Record<string, unknown>) => unknown which allows non-LexicalNode classes and forces unsafe casts; change the signature to be generic (e.g. function parseHtmlNode<T extends LexicalNode>(HtmlNode: new (data: Record<string, unknown>) => T) ) so the constructor is constrained to produce a LexicalNode subtype, then update internal variables and return types so the values returned at the places that currently cast/return node (the code paths around the current Line 26 and Line 40) are returned as T directly without unknown casts; ensure any local helpers or variables referencing the constructed node use T/LexicalNode types (and update imports if needed) so type-checking prevents non-Lexical constructors from being passed to parseHtmlNode.packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts (1)
410-410:firstElementChildcast may hide null reference issues.Both lines cast
firstElementChildtoRenderOutput['element'], butfirstElementChildcan benullif the template produces no elements. While this is unlikely given the template structure, the cast bypasses TypeScript's null safety.Suggested defensive check
- return renderWithVisibility({element: emailDiv.firstElementChild as RenderOutput['element']}, node.visibility, options); + const firstChild = emailDiv.firstElementChild; + if (!firstChild) { + return renderEmptyContainer(emailDoc); + } + return renderWithVisibility({element: firstChild as RenderOutput['element']}, node.visibility, options);Also applies to: 424-424
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts` at line 410, The call to renderWithVisibility currently casts emailDiv.firstElementChild to RenderOutput['element'], which hides potential null refs; update where renderWithVisibility is called (using emailDiv.firstElementChild and the other similar occurrence) to first check if firstElementChild is non-null and handle the null case explicitly (e.g., return a fallback RenderOutput, throw a descriptive error, or skip rendering) before calling renderWithVisibility with node.visibility and options so TypeScript null-safety is preserved and runtime NPEs are avoided.packages/kg-default-nodes/src/utils/visibility.ts (1)
38-41: RenderOutput type is well-defined but may be overly strict for some callers.The
RenderOutputinterface requireselementto haveinnerHTML,outerHTML, and optionallyvalue. This works for most DOM elements, but note that callers usingfirstElementChild(likecalltoaction-renderer.ts) may passnullif the template produces no elements—the cast toRenderOutput['element']bypasses this check.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/utils/visibility.ts` around lines 38 - 41, The RenderOutput.element type is too strict and disallows null values returned by DOM lookups (e.g., firstElementChild in calltoaction-renderer.ts); change the RenderOutput interface so element can be null (make element type "(Element & {innerHTML: string; outerHTML: string; value?: string}) | null") and then update callers that cast to RenderOutput['element'] (such as calltoaction-renderer) to handle the null case safely before accessing innerHTML/outerHTML/value.packages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.ts (1)
53-67: Consider sanitizing header/subheaderinnerHTMLassignments.Lines 57 and 65 assign
templateData.headerandtemplateData.subheaderdirectly toinnerHTML. If these values can contain untrusted user input, this creates XSS sinks. This appears to be a pre-existing pattern, but worth noting for a future sanitization pass across renderers.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.ts` around lines 53 - 67, The code assigns untrusted content to innerHTML in header-renderer.ts (in the block using templateData.hasHeader/templateData.hasSubheader), creating XSS risk; update the assignments for headerElement and subheaderElement (currently using headerElement.innerHTML = templateData.header and subheaderElement.innerHTML = templateData.subheader) to a safe approach—either set textContent instead of innerHTML if HTML is not needed, or run templateData.header/templateData.subheader through a trusted sanitizer (e.g., DOMPurify or the project's sanitize utility) before assigning to innerHTML; make the same change for both headerElement and subheaderElement.packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts (1)
140-142: Non-null assertion ongetResizedImageDimensionsreturn value.Line 140 uses
!to assert the result is non-null. WhilegetResizedImageDimensionsshould always return dimensions when given valid input, ifimagehas invalid dimensions (e.g., zero width/height), the function behavior is unclear.Consider adding a guard or verifying the function's contract:
Suggested improvement
- const newImageDimensions = getResizedImageDimensions(image, {width: 600})!; - img.setAttribute('width', String(newImageDimensions.width)); - img.setAttribute('height', String(newImageDimensions.height)); + const newImageDimensions = getResizedImageDimensions(image, {width: 600}); + if (newImageDimensions) { + img.setAttribute('width', String(newImageDimensions.width)); + img.setAttribute('height', String(newImageDimensions.height)); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts` around lines 140 - 142, The code currently uses a non-null assertion on getResizedImageDimensions(image, {width: 600}) in gallery-renderer.ts; instead, call getResizedImageDimensions and store the result in a variable (e.g., const newImageDimensions = getResizedImageDimensions(...)), check if newImageDimensions is truthy before using it, and handle the null case by either skipping setting img.width/height, using a safe fallback size, or logging a warning; update the img.setAttribute calls to only run when newImageDimensions is valid to avoid runtime errors.packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts (1)
8-8: Non-null assertion on regex match is fragile.While the regex
/[^/]*$/technically always matches (it can match an empty string at the end), using!makes the code less defensive. If the regex were ever changed, this could throw.Suggested improvement
- image.fileName = element.src.match(/[^/]*$/)![0]; + image.fileName = element.src.match(/[^/]*$/)?.[0] ?? '';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts` at line 8, Replace the non-null assertion on the regex match by capturing the result of element.src.match(/[^/]*$/) into a variable, check for null/undefined before indexing, and fall back to a safe default (e.g., the original element.src or an empty string) when setting image.fileName; update the assignment that sets image.fileName (the line using element.src.match and image.fileName) to use this guarded logic so it won't throw if the match changes or fails.packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts (1)
17-17: Fallback to empty object may cause unexpected behavior.When
buttonElementis null,buttonStylesbecomes{} as CSSStyleDeclaration. AccessingbuttonStyles.backgroundColorandbuttonStyles.coloron an empty object will returnundefined, which then correctly falls back to the defaults on lines 18-19. However, casting an empty object toCSSStyleDeclarationis misleading and could cause issues if additional style properties are accessed without fallbacks.Consider using optional chaining consistently:
Suggested improvement
- const buttonStyles = buttonElement?.style || {} as CSSStyleDeclaration; - const buttonColor = buttonStyles.backgroundColor || '#000000'; - const buttonTextColor = buttonStyles.color || '#ffffff'; + const buttonColor = buttonElement?.style.backgroundColor || '#000000'; + const buttonTextColor = buttonElement?.style.color || '#ffffff';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts` at line 17, The current line casts an empty object to CSSStyleDeclaration which is misleading and can hide missing properties; change the usage so buttonStyles is either the real CSSStyleDeclaration or undefined (e.g., const buttonStyles = buttonElement?.style) and use optional chaining/nullish coalescing when reading properties (references: buttonStyles, buttonElement in calltoaction-parser.ts) so accesses like buttonStyles?.backgroundColor and buttonStyles?.color safely fall back to defaults without casting an empty object.packages/kg-default-nodes/src/generate-decorator-node.ts (1)
124-128: Falsy check may incorrectly use default for0or empty string values.The condition
data[prop.name] || prop.defaulton line 127 will use the default value whendata[prop.name]is0,'', or other falsy values. While booleans are correctly handled with??on line 125, numeric properties with value0or string properties with empty string''would incorrectly fall back to defaults.This is pre-existing behavior, but the TypeScript migration is a good opportunity to address it:
Suggested fix
internalProps.forEach((prop) => { - if (typeof prop.default === 'boolean') { - this[prop.privateName] = data[prop.name] ?? prop.default; - } else { - this[prop.privateName] = data[prop.name] || prop.default; - } + // Use nullish coalescing for all types to preserve explicit 0, '', false values + this[prop.privateName] = data[prop.name] ?? prop.default; });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/generate-decorator-node.ts` around lines 124 - 128, The assignment branch that sets this[prop.privateName] uses a falsy check (data[prop.name] || prop.default) which incorrectly overrides valid falsy values like 0 or ''. Change the non-boolean branch to use a nullish coalescing-style check so that undefined/null fall back to prop.default but valid falsy values (0, '') are preserved; update the conditional handling around prop.default, prop.name and prop.privateName accordingly (mirror the existing boolean branch's use of ?? for consistency).packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts (1)
25-27: Mutating input parameternode.backgroundColor.The function modifies
node.backgroundColordirectly, which mutates the caller's object. This could cause unexpected side effects if the caller expects the node data to remain unchanged after rendering.Consider either documenting this behavior or working with a local copy:
Suggested improvement
+ let backgroundColor = node.backgroundColor; // backgroundColor can end up with `rgba(0, 0, 0, 0)` from old mobiledoc copy/paste // that is invalid when used in a class name so fall back to `white` when we don't have // something that looks like a valid class - if (!node.backgroundColor || !node.backgroundColor.match(/^[a-zA-Z\d-]+$/)) { - node.backgroundColor = 'white'; + if (!backgroundColor || !backgroundColor.match(/^[a-zA-Z\d-]+$/)) { + backgroundColor = 'white'; } - element.classList.add('kg-card', 'kg-callout-card', `kg-callout-card-${node.backgroundColor}`); + element.classList.add('kg-card', 'kg-callout-card', `kg-callout-card-${backgroundColor}`);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts` around lines 25 - 27, The code directly mutates node.backgroundColor; avoid mutating the caller's object by validating into a local value or a shallow copy instead: e.g., create a local variable (const backgroundColor = (node.backgroundColor && node.backgroundColor.match(/^[a-zA-Z\d-]+$/)) ? node.backgroundColor : 'white') or clone the node (const safeNode = { ...node }) and update safeNode.backgroundColor, then use backgroundColor/safeNode for rendering; update the logic around node.backgroundColor validation so no assignment is made to the original node object.packages/kg-default-nodes/test/nodes/gallery.test.ts (1)
637-1030: Reduce repeatedas unknown as LexicalEditorcasts in exportDOM calls.These casts hide type mismatches and make the tests harder to trust. Prefer passing correctly typed export options (or a helper wrapper with a single cast point) instead of per-call double-casts.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/gallery.test.ts` around lines 637 - 1030, Tests call galleryNode.exportDOM(... as unknown as LexicalEditor) many times; instead create a single correctly typed export options value or a small test helper to centralize the cast so individual calls don't repeat "as unknown as LexicalEditor". Update references to exportOptions and calls to exportDOM to use the properly typed variable (or a helper function like renderExport(node, opts) that performs one cast), and remove the per-call double-casts in tests that call exportDOM.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/kg-default-nodes/src/nodes/html/html-parser.ts`:
- Around line 12-22: Pre-scan for the closing kg-card-end comment before
mutating: starting from nextNode, walk siblings using isHtmlEndComment to find
the matching end comment and if none is found, skip the destructive while loop
entirely so you don't accumulate unrelated nodes into html; only after
confirming the end marker exists, run the existing loop that uses currentNode,
checks nodeType, pushes into the html array, and calls currentNode.remove().
In `@packages/kg-default-nodes/src/nodes/signup/SignupNode.ts`:
- Around line 108-110: The factory $createSignupNode currently accepts
Record<string, unknown> while it constructs new SignupNode with a SignupData;
change $createSignupNode's parameter type to SignupData (or a compatible
narrower type/interface) so the factory signature matches SignupNode's
constructor, and update any callers if needed to pass the correct typed object;
refer to $createSignupNode and the SignupNode constructor/SignupData type when
making the change.
In `@packages/kg-default-nodes/src/nodes/transistor/TransistorNode.ts`:
- Around line 58-60: Update the type guard function $isTransistorNode so it
returns a type predicate (node is TransistorNode) instead of plain boolean;
locate the exported function $isTransistorNode and change its signature to use
the TypeScript type predicate while keeping the parameter as node: unknown and
the runtime check node instanceof TransistorNode unchanged so TypeScript will
narrow types after the guard.
- Line 1: Replace the root lodash named import with a deep-path default import
to reduce bundle size: change the import of cloneDeep in TransistorNode.ts (the
current "import {cloneDeep} from 'lodash'") to the deep path default form
("import cloneDeep from 'lodash/cloneDeep'") so only the needed utility is
bundled; leave all usages of cloneDeep in the file (e.g., anywhere it's called)
unchanged.
In `@packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts`:
- Line 39: The code retrieves baseSrc via el.getAttribute('data-src') and then
uses new URL(baseSrc!) and other non-null assertions; change this to explicitly
check that baseSrc is a non-empty string (e.g., if (!baseSrc) return or handle
fallback) before calling new URL(baseSrc) and remove the non-null assertions;
update all usages in this module (the baseSrc variable and the locations
currently asserting with !) so you validate/guard once and then safely use
baseSrc without the ! operator.
---
Outside diff comments:
In `@packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts`:
- Around line 184-217: The emailTemplate function injects options.postUrl into
multiple anchor hrefs regardless of presence, causing href="undefined"; update
emailTemplate to guard all uses of RenderOptions.postUrl (e.g. the anchors
around the thumbnail, title, play button and duration) by either conditionally
omitting the href attribute when postUrl is falsy or substituting a safe
fallback (e.g. '#' or empty string) so anchors are not rendered with
"undefined"; check occurrences in emailTemplate (thumbnail anchor, title anchor,
play-button anchor, duration anchor) and use a single pattern like postUrl ?
`href="${options.postUrl}"` : '' to keep markup valid while preserving classes
and getFormattedDuration(node.duration) usage.
In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts`:
- Around line 27-31: The imageData type incorrectly allows imageUrl to be a
number; update the declaration so imageUrl is string only (remove | number) to
match readImageAttributesFromElement which returns a string and the assignment
at imageData.imageUrl; adjust the type signature where imageData is defined and
any related uses to ensure imageUrl is typed as string while leaving imageWidth
and imageHeight as string | number | null.
---
Duplicate comments:
In `@packages/kg-default-nodes/package.json`:
- Around line 19-21: The build, prepare, and pretest npm scripts ("build",
"prepare", "pretest") do not clean the build/ directory first, which allows
stale artifacts to be published; update each script to remove or clean build/
before running tsc (e.g., run a cleanup command such as rimraf build or rm -rf
build or call an existing "clean" npm script) so that build/ is freshly created
prior to the existing tsc && tsc -p ... && echo ... sequence.
In `@packages/kg-default-nodes/src/nodes/file/file-renderer.ts`:
- Around line 55-68: The template emits empty href attributes when
options.postUrl is unset; update all href uses that currently use
escapeHtml(options.postUrl || '') to fall back to node.src instead (use
escapeHtml(options.postUrl || node.src)) so links for file title, caption, meta
and thumbnail point to node.src when postUrl is missing; locate these
occurrences in file-renderer.ts where hrefs wrap node.fileTitle,
node.fileCaption, node.fileName and the thumbnail anchor and change the fallback
accordingly.
In `@packages/kg-default-nodes/src/nodes/product/product-renderer.ts`:
- Around line 64-90: The cardTemplate function is inserting raw data into an
HTML string which is then used with innerHTML, creating an XSS risk; update
cardTemplate to sanitize/escape all interpolated values (productTitle,
productDescription, productButton, productUrl, productImageSrc,
productImageWidth, productImageHeight and any starIcon/star class strings)
before building the markup or, better, switch to constructing DOM nodes
programmatically and set textContent/attribute values rather than embedding raw
values in a template string; add or reuse a single escape/sanitize helper (e.g.,
escapeHtml or sanitizeUrl) and apply it to attribute and text interpolations
used by cardTemplate (and the other template blocks referenced in the comment).
In `@packages/kg-default-nodes/test/nodes/gallery.test.ts`:
- Around line 627-629: The test currently indexes nodes[0] which can throw when
$generateNodesFromDOM(editor, document) returns an empty array; instead assert
that no returned node is a gallery by replacing the length/index checks with a
check on the nodes array (e.g., using Array.prototype.every or
Array.prototype.some) to ensure every GalleryNode from $generateNodesFromDOM has
getType() !== 'gallery' — update the assertions around the nodes variable and
references to GalleryNode and getType() accordingly.
In `@packages/kg-default-nodes/test/nodes/header.test.ts`:
- Around line 151-159: Replace the no-op "void element" with a real assertion
that verifies the renderer's current empty-container behavior: call
$createHeaderNode(...) and node.exportDOM(...), then assert that the returned
element is a DOM Element (not null) and that it contains no meaningful content
(e.g., has no child elements and empty/textContent trimmed is empty) to
explicitly validate the behavior of the header renderer in
packages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.ts and
the exportDOM path.
- Around line 468-476: Test currently discards the exportDOM result with "void
element" making the case a no-op; instead, update the spec using
$createHeaderNode and node.exportDOM(exportOptions) to assert the actual
empty-content behavior produced by the v2 header renderer (the code path in the
header renderer that handles header/subheader null + buttonEnabled=false).
Replace the no-op with an assertion that matches the renderer's real return for
empty content (for example assert that element is a DocumentFragment or Element
with no children and no textContent / innerHTML), or explicitly assert the
expected element type and that its childNodes.length === 0.
---
Nitpick comments:
In `@packages/kg-default-nodes/src/generate-decorator-node.ts`:
- Around line 124-128: The assignment branch that sets this[prop.privateName]
uses a falsy check (data[prop.name] || prop.default) which incorrectly overrides
valid falsy values like 0 or ''. Change the non-boolean branch to use a nullish
coalescing-style check so that undefined/null fall back to prop.default but
valid falsy values (0, '') are preserved; update the conditional handling around
prop.default, prop.name and prop.privateName accordingly (mirror the existing
boolean branch's use of ?? for consistency).
In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts`:
- Line 17: The current line casts an empty object to CSSStyleDeclaration which
is misleading and can hide missing properties; change the usage so buttonStyles
is either the real CSSStyleDeclaration or undefined (e.g., const buttonStyles =
buttonElement?.style) and use optional chaining/nullish coalescing when reading
properties (references: buttonStyles, buttonElement in calltoaction-parser.ts)
so accesses like buttonStyles?.backgroundColor and buttonStyles?.color safely
fall back to defaults without casting an empty object.
In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts`:
- Line 410: The call to renderWithVisibility currently casts
emailDiv.firstElementChild to RenderOutput['element'], which hides potential
null refs; update where renderWithVisibility is called (using
emailDiv.firstElementChild and the other similar occurrence) to first check if
firstElementChild is non-null and handle the null case explicitly (e.g., return
a fallback RenderOutput, throw a descriptive error, or skip rendering) before
calling renderWithVisibility with node.visibility and options so TypeScript
null-safety is preserved and runtime NPEs are avoided.
In `@packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts`:
- Around line 25-27: The code directly mutates node.backgroundColor; avoid
mutating the caller's object by validating into a local value or a shallow copy
instead: e.g., create a local variable (const backgroundColor =
(node.backgroundColor && node.backgroundColor.match(/^[a-zA-Z\d-]+$/)) ?
node.backgroundColor : 'white') or clone the node (const safeNode = { ...node })
and update safeNode.backgroundColor, then use backgroundColor/safeNode for
rendering; update the logic around node.backgroundColor validation so no
assignment is made to the original node object.
In `@packages/kg-default-nodes/src/nodes/embed/embed-parser.ts`:
- Around line 90-92: The parser currently mutates the input DOM by assigning to
iframe.src when normalizing protocol-relative URLs; instead, read iframe.src
into a local variable (e.g., normalizedSrc), normalize that variable (if it
matches /^\/\// prefix, prepend "https:"), and use the local normalizedSrc for
any further processing or returned values rather than writing back to iframe.src
in embed-parser.ts (look for the iframe.src handling block).
In `@packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts`:
- Line 8: Replace the non-null assertion on the regex match by capturing the
result of element.src.match(/[^/]*$/) into a variable, check for null/undefined
before indexing, and fall back to a safe default (e.g., the original element.src
or an empty string) when setting image.fileName; update the assignment that sets
image.fileName (the line using element.src.match and image.fileName) to use this
guarded logic so it won't throw if the match changes or fails.
In `@packages/kg-default-nodes/src/nodes/gallery/gallery-renderer.ts`:
- Around line 140-142: The code currently uses a non-null assertion on
getResizedImageDimensions(image, {width: 600}) in gallery-renderer.ts; instead,
call getResizedImageDimensions and store the result in a variable (e.g., const
newImageDimensions = getResizedImageDimensions(...)), check if
newImageDimensions is truthy before using it, and handle the null case by either
skipping setting img.width/height, using a safe fallback size, or logging a
warning; update the img.setAttribute calls to only run when newImageDimensions
is valid to avoid runtime errors.
In `@packages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.ts`:
- Around line 53-67: The code assigns untrusted content to innerHTML in
header-renderer.ts (in the block using
templateData.hasHeader/templateData.hasSubheader), creating XSS risk; update the
assignments for headerElement and subheaderElement (currently using
headerElement.innerHTML = templateData.header and subheaderElement.innerHTML =
templateData.subheader) to a safe approach—either set textContent instead of
innerHTML if HTML is not needed, or run
templateData.header/templateData.subheader through a trusted sanitizer (e.g.,
DOMPurify or the project's sanitize utility) before assigning to innerHTML; make
the same change for both headerElement and subheaderElement.
In `@packages/kg-default-nodes/src/nodes/html/html-parser.ts`:
- Line 3: The parseHtmlNode function currently accepts a constructor typed as
new (data: Record<string, unknown>) => unknown which allows non-LexicalNode
classes and forces unsafe casts; change the signature to be generic (e.g.
function parseHtmlNode<T extends LexicalNode>(HtmlNode: new (data:
Record<string, unknown>) => T) ) so the constructor is constrained to produce a
LexicalNode subtype, then update internal variables and return types so the
values returned at the places that currently cast/return node (the code paths
around the current Line 26 and Line 40) are returned as T directly without
unknown casts; ensure any local helpers or variables referencing the constructed
node use T/LexicalNode types (and update imports if needed) so type-checking
prevents non-Lexical constructors from being passed to parseHtmlNode.
In `@packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts`:
- Around line 22-25: The inline complex return type of
BrowserDocument.createElement should be extracted into a named interface to
improve readability and reuse: define a new interface (e.g., CanvasElement or
CreateElementReturn) that extends BrowserElement and declares innerHTML, width,
height, getContext(...) returning the typed context and getImageData(...)
returning the data array, then replace the inline return type in the
BrowserDocument interface with this new interface; update any other uses of the
same inline shape (if present) to reference the new interface and keep names
BrowserDocument and createElement unchanged.
In `@packages/kg-default-nodes/src/utils/visibility.ts`:
- Around line 38-41: The RenderOutput.element type is too strict and disallows
null values returned by DOM lookups (e.g., firstElementChild in
calltoaction-renderer.ts); change the RenderOutput interface so element can be
null (make element type "(Element & {innerHTML: string; outerHTML: string;
value?: string}) | null") and then update callers that cast to
RenderOutput['element'] (such as calltoaction-renderer) to handle the null case
safely before accessing innerHTML/outerHTML/value.
In `@packages/kg-default-nodes/test/nodes/gallery.test.ts`:
- Around line 637-1030: Tests call galleryNode.exportDOM(... as unknown as
LexicalEditor) many times; instead create a single correctly typed export
options value or a small test helper to centralize the cast so individual calls
don't repeat "as unknown as LexicalEditor". Update references to exportOptions
and calls to exportDOM to use the properly typed variable (or a helper function
like renderExport(node, opts) that performs one cast), and remove the per-call
double-casts in tests that call exportDOM.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
3ab9780 to
7bfcd68
Compare
- Remove Rollup build (rollup.config.mjs, rollup-plugin-svg, @babel/*) - Move lib/ to src/, rename .js to .ts - Add tsconfig.json (strict, NodeNext, ESM) - Add "type": "module" to package.json - Convert 100 source files and 36 test files to ESM with Lexical types - Inline SVG import in AtLinkNode (replaces rollup-plugin-svg) - Replace .eslintrc.js with eslint.config.js (flat config) - Output to build/ via tsc (replaces cjs/ and es/ dirs) - 668 tests passing
7bfcd68 to
f7a112c
Compare
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/kg-default-nodes/src/nodes/video/video-parser.ts (1)
29-35:⚠️ Potential issue | 🟡 Minor
catchhere never filters invalid durations.
parseInt()does not throw, so malformed values still assignpayload.duration = NaNat Line 32. Parse both parts explicitly and only write the field when both numbers are finite.Proposed fix
if (durationText) { const [minutes, seconds] = durationText.split(':'); - try { - payload.duration = parseInt(minutes) * 60 + parseInt(seconds); - } catch { - // ignore duration + const parsedMinutes = Number.parseInt(minutes ?? '', 10); + const parsedSeconds = Number.parseInt(seconds ?? '', 10); + + if (Number.isFinite(parsedMinutes) && Number.isFinite(parsedSeconds)) { + payload.duration = (parsedMinutes * 60) + parsedSeconds; } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/video/video-parser.ts` around lines 29 - 35, The current duration parsing uses parseInt inside a try/catch but parseInt never throws, so invalid inputs set payload.duration to NaN; update the block that reads durationText (the variables minutes/seconds and assignment to payload.duration) to explicitly parse and validate both parts (e.g., use Number or parseInt then check !Number.isNaN and Number.isFinite for minutes and seconds) and only set payload.duration when both parsed values are valid numbers (otherwise leave payload.duration unset or handle as invalid). Ensure you reference durationText, payload.duration, and the minutes/seconds variables when making the change.
♻️ Duplicate comments (8)
packages/kg-default-nodes/src/nodes/html/html-parser.ts (1)
12-22:⚠️ Potential issue | 🟠 MajorGuard for missing
kg-card-endbefore destructive sibling removal.If no end marker exists, Line 12-Line 22 removes unrelated trailing siblings and folds them into one HTML card. Pre-scan for an end node first, then mutate only when found.
Proposed fix
conversion(domNode: Node) { const html = []; let nextNode = domNode.nextSibling; + let endNode = nextNode; + + while (endNode && !isHtmlEndComment(endNode)) { + endNode = endNode.nextSibling; + } + + if (!endNode) { + return null; + } - while (nextNode && !isHtmlEndComment(nextNode)) { + while (nextNode && nextNode !== endNode) { const currentNode = nextNode; nextNode = currentNode.nextSibling; if (currentNode.nodeType === 1) { html.push((currentNode as Element).outerHTML); } else if (currentNode.nodeType === 3 && currentNode.textContent) { html.push(currentNode.textContent); } // remove nodes as we go so that they don't go through the parser currentNode.remove(); } + endNode.remove(); const payload: Record<string, unknown> = {html: html.join('\n').trim()}; const node = new HtmlNode(payload); return {node: node as LexicalNode}; },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/html/html-parser.ts` around lines 12 - 22, The loop in html-parser.ts that collects and removes siblings (using nextNode/currentNode, currentNode.remove(), (currentNode as Element).outerHTML and textContent) can run past a missing kg-card-end and destructively remove unrelated nodes; pre-scan from the starting sibling using isHtmlEndComment to find the end marker first, and only if an end node is found perform the existing collection/removal loop; otherwise leave siblings untouched and skip creating the fused HTML card.packages/kg-default-nodes/src/nodes/signup/SignupNode.ts (1)
108-110:⚠️ Potential issue | 🟠 MajorFactory input type still bypasses the stricter node input contract.
Line 108 uses
Record<string, unknown>, which is broader than the constructor’sSignupDataand weakens field-level safety for node creation.Suggested fix
-export const $createSignupNode = (dataset: Record<string, unknown>) => { +export const $createSignupNode = (dataset: SignupData = {}) => { return new SignupNode(dataset); };#!/bin/bash # Verify the mismatch and check for similar factory signatures in nodes. rg -nP --type=ts '\$createSignupNode\s*=\s*\(dataset:\s*Record<string,\s*unknown>\)' packages/kg-default-nodes/src/nodes/signup/SignupNode.ts rg -nP --type=ts 'constructor\s*\(\{[^)]*\}\s*:\s*SignupData' packages/kg-default-nodes/src/nodes/signup/SignupNode.ts rg -nP --type=ts '\$create[A-Za-z0-9_]*Node\s*=\s*\([^)]*Record<string,\s*unknown>' packages/kg-default-nodes/src/nodes🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/signup/SignupNode.ts` around lines 108 - 110, The factory $createSignupNode currently accepts a broad Record<string, unknown>, which bypasses the constructor's stricter SignupData contract; change the parameter type of $createSignupNode from Record<string, unknown> to SignupData (importing SignupData if needed) so it matches SignupNode's constructor signature, update any call sites/tests that pass loose objects to satisfy the stronger type, and run a repo-wide grep for other $create*Node factory signatures to make similar fixes for consistency.packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts (1)
404-418:⚠️ Potential issue | 🟠 MajorSanitize
sponsorLabelbefore the email early return.The cleanup at Lines 415-418 only runs on the web path. When
options.target === 'email', Line 410 returns first, soemailCTATemplate()still interpolates the rawdataset.sponsorLabel.Proposed fix
export function renderCallToActionNode(node: CTANodeData, options: CTARenderOptions = {}) { addCreateDocumentOption(options); const document = options.createDocument!(); const dataset = { @@ linkColor: node.linkColor }; + + if (dataset.hasSponsorLabel) { + const cleanBasicHtml = buildCleanBasicHtmlForElement(document.createElement('div')); + const cleanedHtml = cleanBasicHtml(dataset.sponsorLabel, {firstChildInnerContent: true}); + dataset.sponsorLabel = cleanedHtml || ''; + } // Add validation for backgroundColor @@ if (options.target === 'email') { const emailDoc = options.createDocument!(); const emailDiv = emailDoc.createElement('div'); @@ - if (dataset.hasSponsorLabel) { - const cleanBasicHtml = buildCleanBasicHtmlForElement(element); - const cleanedHtml = cleanBasicHtml(dataset.sponsorLabel, {firstChildInnerContent: true}); - dataset.sponsorLabel = cleanedHtml || ''; - } const htmlString = ctaCardTemplate(dataset);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts` around lines 404 - 418, The email path returns early using emailCTATemplate but doesn't sanitize dataset.sponsorLabel; move or duplicate the cleaning logic so sponsorLabel is sanitized before the email branch executes: when options.target === 'email', call buildCleanBasicHtmlForElement(element) and cleanBasicHtml(dataset.sponsorLabel, {firstChildInnerContent: true}) (same flow used later), assign the cleaned string back to dataset.sponsorLabel (or a local sanitized copy) prior to calling emailCTATemplate/createDocument/renderWithVisibility so the email output uses the cleaned sponsorLabel.packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts (2)
47-49:⚠️ Potential issue | 🟡 MinorHandle
undefinedcaption to avoid string concatenation issues.
readCaptionFromElement(domNode)can returnundefined. When the initial caption isundefinedand a subsequent sibling has a caption, line 63 produces"undefined / actualCaption".Initialize the caption to an empty string or filter before concatenating:
Proposed fix
conversion(domNode: HTMLElement) { const payload: Record<string, unknown> = { - caption: readCaptionFromElement(domNode) + caption: readCaptionFromElement(domNode) ?? '' };Or use conditional concatenation:
const currentNodeCaption = readCaptionFromElement(currentNode); if (currentNodeCaption) { - payload.caption = `${payload.caption} / ${currentNodeCaption}`; + payload.caption = payload.caption ? `${payload.caption} / ${currentNodeCaption}` : currentNodeCaption; }Also applies to: 61-64
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts` around lines 47 - 49, The payload construction uses readCaptionFromElement(domNode) which can return undefined, causing later concatenation in gallery-caption-building logic (see readCaptionFromElement and the payload variable) to produce "undefined / actualCaption"; fix by normalizing the caption to an empty string (or filtering out falsy values) before storing in payload.caption so subsequent concatenation or joining code never sees undefined—update the payload assignment and any concatenation that uses payload.caption (or siblingCaption assembly logic) to use the normalized string or skip empty parts.
99-110:⚠️ Potential issue | 🟡 MinorEmpty
srcfallback may create invalid gallery images.When
data-srcis missing and there's a<noscript>sibling, line 102 setssrcto an empty string. This passes the undefined filter at line 110 but results in an invalid image URL being processed byreadGalleryImageAttributesFromElement.Consider extracting the actual
srcfrom the<noscript>image or returningundefinedto skip the entry:Proposed fix
if (img.previousElementSibling?.tagName === 'NOSCRIPT' && img.previousElementSibling.getElementsByTagName('img').length) { const prevNode = img.previousElementSibling; - img.setAttribute('src', img.getAttribute('data-src') ?? ''); + const noscriptImg = prevNode.getElementsByTagName('img')[0]; + const src = img.getAttribute('data-src') ?? noscriptImg?.getAttribute('src'); + if (!src) { + return undefined; + } + img.setAttribute('src', src); prevNode.remove(); } else { return undefined; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts` around lines 99 - 110, The current map/filter over gallery images may set img.src to an empty string when data-src is missing, producing invalid entries for downstream readGalleryImageAttributesFromElement; update the logic in the gallery-parser mapping: when img.getAttribute('src') is falsy and img.previousElementSibling?.tagName === 'NOSCRIPT', attempt to find an <img> inside that NOSCRIPT and extract its actual src (e.g., previousElementSibling.getElementsByTagName('img')[0].getAttribute('src')) and set that as img.src only if present; if no valid src can be obtained from data-src or the NOSCRIPT image, return undefined so the subsequent .filter(img => img !== undefined) will drop it. Ensure references to img, previousElementSibling, and readGalleryImageAttributesFromElement remain intact.packages/kg-default-nodes/test/nodes/header.test.ts (1)
151-160:⚠️ Potential issue | 🟡 MinorThese “renders nothing” tests still don’t validate behavior.
Both blocks end in
void element, so they pass without asserting expected output.Proposed fix (assert current behavior explicitly)
-const {element} = node.exportDOM(exportOptions as unknown as LexicalEditor); -// NOTE: pre-existing issue — renderer returns an element, not null. -// Original JS test used `element.should.be.null` (a no-op getter). -void element; +const {element} = node.exportDOM(exportOptions as unknown as LexicalEditor); +(element as HTMLElement).outerHTML.should.containEql('kg-header-card');Also applies to: 468-477
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/header.test.ts` around lines 151 - 160, The tests currently end with "void element" so they don't assert anything; update the two "renders nothing" cases (the one using $createHeaderNode and the duplicate at lines ~468-477) to explicitly assert the current renderer behavior instead of no-oping: call node.exportDOM(exportOptions as unknown as LexicalEditor) and then assert that the returned element is not null and matches the expected shape (e.g., is an Element/HTMLElement and either has no children or the expected empty structure) so the test actually verifies the outcome of exportDOM; use the $createHeaderNode, node.header/node.subheader/buttonEnabled, exportOptions and exportDOM symbols to locate and modify the assertions.packages/kg-default-nodes/package.json (1)
19-21:⚠️ Potential issue | 🟠 MajorClean
build/before compiling.
tscwill not remove outputs for renamed or deleted sources. Since this package now publishes the wholebuilddirectory, stale JS /.d.tsfiles can survive local rebuilds and get shipped.🧹 Suggested script update
"scripts": { + "clean": "node -e \"require('node:fs').rmSync('build', {recursive: true, force: true})\"", - "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", + "build": "yarn clean && tsc && tsc -p tsconfig.cjs.json && echo '{\"type\":\"module\"}' > build/esm/package.json", + "prepare": "yarn build", + "pretest": "yarn build && tsc -p tsconfig.test.json",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/package.json` around lines 19 - 21, The build/ directory isn't cleaned before running tsc so stale JS/.d.ts files can persist; update the package.json scripts ("build", "prepare", and "pretest") to remove the existing build directory first (e.g., run a clean step such as rm -rf build or use rimraf) before running your tsc commands and creating build/esm/package.json so each build starts from a clean output tree and won't publish leftover artifacts.packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts (1)
51-55:⚠️ Potential issue | 🟡 MinorKeep the thumbnail dimension guard strictly positive.
This branch now filters out missing values, but negative numbers are still truthy and will produce a negative
spacerHeight/VML size. Please keep the width/height check as> 0before dividing.Suggested fix
- if (isEmail && isVideoWithThumbnail && metadata.thumbnail_width && metadata.thumbnail_height) { + if ( + isEmail && + isVideoWithThumbnail && + typeof metadata.thumbnail_width === 'number' && + typeof metadata.thumbnail_height === 'number' && + metadata.thumbnail_width > 0 && + metadata.thumbnail_height > 0 + ) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts` around lines 51 - 55, The thumbnail dimension guard currently checks thumbnail_width/thumbnail_height truthiness which allows negative values; update the condition in the isEmail && isVideoWithThumbnail branch to require metadata.thumbnail_width > 0 and metadata.thumbnail_height > 0 before computing thumbnailAspectRatio, spacerWidth, and spacerHeight so you never divide by or produce negative VML sizes (reference variables: metadata.thumbnail_width, metadata.thumbnail_height, thumbnailAspectRatio, spacerWidth, spacerHeight).
🧹 Nitpick comments (8)
packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts (1)
22-25: SplitcreateElementtyping by tag instead of using one broad intersection.Line 24 currently implies every created element has both canvas and script members, which can mask invalid usage. Prefer overloads for
'canvas'and'script'to keep type checks meaningful.Refactor sketch
interface BrowserDocument { currentScript: { parentElement: BrowserElement } | null; - createElement(tag: string): BrowserElement & { innerHTML: string; width: number; height: number; getContext(id: string): { fillStyle: string; fillRect(x: number, y: number, w: number, h: number): void; getImageData(x: number, y: number, w: number, h: number): { data: number[] } } }; + createElement(tag: 'script'): BrowserElement & {innerHTML: string}; + createElement(tag: 'canvas'): BrowserElement & { + width: number; + height: number; + getContext(id: string): { + fillStyle: string; + fillRect(x: number, y: number, w: number, h: number): void; + getImageData(x: number, y: number, w: number, h: number): {data: number[]}; + }; + }; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts` around lines 22 - 25, The createElement signature on the BrowserDocument interface currently returns a broad intersection that makes every element look like both a canvas and a script; update BrowserDocument.createElement to use overloads (or a discriminated union) so calling createElement('canvas') returns a canvas-like BrowserElement with canvas-specific members (width, height, getContext(...), getImageData(...)) and createElement('script') returns a script-like BrowserElement with currentScript/parentElement and innerHTML, keeping the BrowserElement base separate; change the type for createElement in the BrowserDocument interface and adjust any dependent code to rely on the specific overloads rather than the combined intersection.packages/kg-default-nodes/test/nodes/email-cta.test.ts (1)
213-214: Remove incorrectLexicalEditorinput cast fromexportDOMcalls; assert return type instead.These 10 call sites cast an options object to
LexicalEditor, which is semantically incorrect—the method expectsRecord<string, unknown>, not an editor instance. Removing the input cast and relying on the method's actual signature improves type clarity.Suggested pattern:
-const {element} = emailNode.exportDOM({...exportOptions, ...options} as unknown as LexicalEditor); -const el = element as HTMLElement; +const {element} = emailNode.exportDOM({...exportOptions, ...options}); +const el = element as HTMLElement;The return type is
{ element: Element | null; type?: string }, so theas HTMLElementcast on the second line remains necessary to narrow the element type.Also applies to: 240–241, 262–263, 284–285, 306–307, 334–335, 362–363, 402–403, 440–441, 477–478
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/email-cta.test.ts` around lines 213 - 214, Remove the incorrect cast of the options object to LexicalEditor when calling emailNode.exportDOM; exportDOM expects a Record<string, unknown> so stop casting {...exportOptions, ...options} as unknown as LexicalEditor and pass the merged object directly, and instead assert/narrow the return using the known shape ({ element: Element | null; type?: string }) by keeping the subsequent `as HTMLElement` cast on the `element` variable; update all similar call sites (the ones around lines referenced) to call exportDOM(...) without the LexicalEditor cast and rely on the exportDOM return type assertion.packages/kg-default-nodes/test/nodes/video.test.ts (1)
245-247: Remove incorrectLexicalEditorcast fromexportDOMcalls in video tests.The pattern
exportDOM(exportOptions as unknown as LexicalEditor)misrepresents the parameter type. SinceexportOptionsis already correctly typed asRecord<string, unknown>to match theexportDOM(options: Record<string, unknown>)signature, the cast hides type issues rather than resolving them.Suggested pattern
-const {element} = videoNode.exportDOM(exportOptions as unknown as LexicalEditor); +const {element} = videoNode.exportDOM(exportOptions) as {element: HTMLElement};Applies to lines 245, 317, 337, 353, 369.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/video.test.ts` around lines 245 - 247, The tests are using an incorrect cast by calling exportDOM(exportOptions as unknown as LexicalEditor); remove the bogus LexicalEditor cast and pass exportOptions directly to exportDOM since the method signature is exportDOM(options: Record<string, unknown>); update all occurrences (e.g., calls on videoNode.exportDOM and related nodes at the locations flagged) to call exportDOM(exportOptions) so the real types are used and any underlying type issues surface.packages/kg-default-nodes/test/nodes/horizontalrule.test.ts (1)
47-49: Cast the result ofexportDOM, not the parameter, for consistency with other tests.Line 47 casts
exportOptionstoLexicalEditorwhich contradicts the actual parameter type. Most test files already cast the return value instead. Apply the same pattern here:Proposed fix
-const {element} = hrNode.exportDOM(exportOptions as unknown as LexicalEditor); -(element as HTMLElement).outerHTML.should.prettifyTo(html` +const {element} = hrNode.exportDOM(exportOptions as unknown as LexicalEditor) as {element: HTMLElement}; +element.outerHTML.should.prettifyTo(html`This aligns with the majority pattern in the codebase (bookmark.test.ts, button.test.ts, etc.) and removes the unnecessary element cast on line 48.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/horizontalrule.test.ts` around lines 47 - 49, The test currently casts the exportOptions argument to LexicalEditor when calling hrNode.exportDOM; instead, cast the return value of hrNode.exportDOM to the expected type (e.g., { element } as unknown as { element: HTMLElement }) to match other tests, remove the unnecessary (element as HTMLElement) cast on the next line, and ensure the constructed element variable is used directly in the prettify assertion; reference hrNode.exportDOM, exportOptions, and element to locate and update the code.packages/kg-default-nodes/src/nodes/callout/callout-parser.ts (1)
7-25: Tighten the constructor signature to eliminate the unnecessary cast.
new (...) => unknownpaired withnode as LexicalNodediscards the type guarantee that this conversion produces a Lexical node. Since all node classes created bygenerateDecoratorNode()extendLexicalNode(throughKoenigDecoratorNode→DecoratorNode), the parameter type can be tightened tonew (data: Record<string, unknown>) => LexicalNodeand the cast removed. This pattern applies consistently across all parser functions in the codebase.♻️ Suggested signature change
-export function parseCalloutNode(CalloutNode: new (data: Record<string, unknown>) => unknown) { +export function parseCalloutNode(CalloutNode: new (data: Record<string, unknown>) => LexicalNode) { return { div: (nodeElem: HTMLElement) => { const isKgCalloutCard = nodeElem.classList?.contains('kg-callout-card'); if (nodeElem.tagName === 'DIV' && isKgCalloutCard) { return { conversion(domNode: HTMLElement) { const textNode = domNode?.querySelector('.kg-callout-text'); const emojiNode = domNode?.querySelector('.kg-callout-emoji'); const color = getColorTag(domNode); const payload: Record<string, unknown> = { calloutText: textNode && textNode.innerHTML.trim() || '', calloutEmoji: emojiNode && emojiNode.innerHTML.trim() || '', backgroundColor: color }; const node = new CalloutNode(payload); - return {node: node as LexicalNode}; + return {node}; }, priority: 1 as const }; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/callout/callout-parser.ts` around lines 7 - 25, The constructor signature for CalloutNode in parseCalloutNode is too loose and forces a runtime cast to LexicalNode; change the parameter type from "new (data: Record<string, unknown>) => unknown" to "new (data: Record<string, unknown>) => LexicalNode" so the created instance is statically known to be a LexicalNode and remove the "as LexicalNode" cast; apply the same tightening to other parser functions that construct nodes produced by generateDecoratorNode / KoenigDecoratorNode / DecoratorNode so all constructors return LexicalNode types.packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts (2)
50-51: Inconsistentsuper()call — same pattern issue as CalloutNode.Since
BookmarkNodemanually initializes all properties (lines 52-59), the empty object passed tosuper({}, key)is unused. Consider callingsuper(key)directly for consistency with the base class.Suggested change
constructor({url, metadata, caption}: BookmarkData = {}, key?: string) { - super({}, key); + super(key); this.__url = url || '';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts` around lines 50 - 51, The BookmarkNode constructor currently calls super({}, key) even though BookmarkNode manually initializes all properties; change the constructor to call super(key) instead so the base class receives the intended key parameter and the unused empty props object is removed—update the BookmarkNode constructor (constructor({url, metadata, caption}: BookmarkData = {}, key?: string)) to pass only key to super and leave the existing manual initialization of url, metadata, caption intact.
114-116: Factory function parameter type mismatch with constructor.Same pattern as
CalloutNode: the constructor acceptsBookmarkData, but the factory function is typed asRecord<string, unknown>. Consider aligning the types for better type safety.Suggested fix
-export const $createBookmarkNode = (dataset: Record<string, unknown>) => { +export const $createBookmarkNode = (dataset: BookmarkData = {}) => { return new BookmarkNode(dataset); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts` around lines 114 - 116, The factory $createBookmarkNode currently types its parameter as Record<string, unknown> but constructs a new BookmarkNode which expects BookmarkData; update the factory signature to accept BookmarkData (or make BookmarkNode constructor accept Record<string, unknown>) so types align—specifically change the $createBookmarkNode parameter type to BookmarkData to match the BookmarkNode constructor and ensure callers pass the correct shape.packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts (1)
27-28: Inconsistentsuper()call — consider passingkeyonly.The base class constructor in
generate-decorator-node.tsisconstructor(data: Record<string, unknown> = {}, key?: string)and callssuper(key). SinceCalloutNodemanually initializes all properties (lines 29-31), the empty object passed tosuper({}, key)is effectively ignored. For consistency with the base class pattern, consider callingsuper(key)directly.Suggested change
constructor({calloutText, calloutEmoji, backgroundColor}: CalloutData = {}, key?: string) { - super({}, key); + super(key); this.__calloutText = calloutText || '';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts` around lines 27 - 28, The CalloutNode constructor currently calls super({}, key) which is inconsistent with the base class constructor pattern in generate-decorator-node.ts (constructor(data: Record<string, unknown> = {}, key?: string) that itself calls super(key)); update the CalloutNode constructor (the constructor({calloutText, calloutEmoji, backgroundColor}: CalloutData = {}, key?: string) in CalloutNode.ts) to call super(key) instead of super({}, key) so you only forward the key to the base class while still initializing calloutText, calloutEmoji and backgroundColor locally.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts`:
- Around line 59-61: exportDOM() currently returns {element: null} in AtLinkNode
(and similarly in AtLinkSearchNode), which violates the expected return type
(HTMLElement | HTMLInputElement | HTMLTextAreaElement) and causes consumers to
dereference a null. Update exportDOM in both AtLinkNode and AtLinkSearchNode to
return a real HTMLElement fallback (e.g., create a span or input element via
document.createElement and set any needed attributes or text/ARIA/data-
attributes to represent the placeholder) so callers like
convert-to-html-string.ts can safely access element.innerHTML/outerHTML without
null checks; ensure the returned object shape matches the declared contract.
In `@packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts`:
- Around line 11-17: RenderOptions currently marks postUrl optional but the
email template interpolates options.postUrl, causing broken hrefs; update the
types and runtime to enforce it: make postUrl a required string on RenderOptions
(change postUrl?: string to postUrl: string) and add a defensive check in the
render function (e.g., renderAudioNode or the renderer function that reads
options.target) that throws or returns a clear error when options.target ===
'email' and options.postUrl is missing/empty, referencing RenderOptions and
options.postUrl/target so callers and the compiler both guarantee a valid
postUrl before composing the email links.
In `@packages/kg-default-nodes/src/nodes/embed/types/twitter.ts`:
- Around line 23-33: The TweetData interface currently marks id as optional
which allows generating malformed Twitter URLs; either make TweetData.id
required (change interface TweetData to id: string) and update any callers to
provide it, or (preferred) add a guard in the email-generation logic that builds
the Twitter status URL: check tweetData?.id (or the local tweetId variable)
before interpolating into "https://twitter.com/.../status/{tweetId}" and if
missing fall back to node.html or skip rendering that URL block; update any
helper that references TweetData.id (e.g., the email template generator that
uses tweetId) to perform this null/undefined check.
In `@packages/kg-default-nodes/src/utils/read-image-attributes-from-element.ts`:
- Around line 20-25: The regex and assignment in
read-image-attributes-from-element accept empty width/height (using (\d*) ) and
write NaN into attrs; change the match to require digits on both sides (e.g.,
use (\d+)x(\d+)) or, if keeping the current regex, validate that match[1] and
match[2] are non-empty before calling parseInt and assigning to
attrs.width/attrs.height so both dimensions must be present. Ensure you
reference the element.getAttribute('data-image-dimensions') check, the match
variable, and the attrs.width/attrs.height assignments when making the change.
In `@packages/kg-default-nodes/src/utils/visibility.ts`:
- Around line 51-60: The function isVisibilityRestricted currently checks old
and new visibility shapes separately, causing legacy objects to be classified as
restricted even though migrateOldVisibilityFormat would normalize them to
unrestricted defaults; fix by first normalizing the input via
migrateOldVisibilityFormat(visibility) and then only evaluate the new-format
conditions (using web?.nonMember, web?.memberSegment !== ALL_MEMBERS_SEGMENT,
email?.memberSegment !== ALL_MEMBERS_SEGMENT), removing the
isOldVisibilityFormat branch so all checks operate on the migrated shape.
---
Outside diff comments:
In `@packages/kg-default-nodes/src/nodes/video/video-parser.ts`:
- Around line 29-35: The current duration parsing uses parseInt inside a
try/catch but parseInt never throws, so invalid inputs set payload.duration to
NaN; update the block that reads durationText (the variables minutes/seconds and
assignment to payload.duration) to explicitly parse and validate both parts
(e.g., use Number or parseInt then check !Number.isNaN and Number.isFinite for
minutes and seconds) and only set payload.duration when both parsed values are
valid numbers (otherwise leave payload.duration unset or handle as invalid).
Ensure you reference durationText, payload.duration, and the minutes/seconds
variables when making the change.
---
Duplicate comments:
In `@packages/kg-default-nodes/package.json`:
- Around line 19-21: The build/ directory isn't cleaned before running tsc so
stale JS/.d.ts files can persist; update the package.json scripts ("build",
"prepare", and "pretest") to remove the existing build directory first (e.g.,
run a clean step such as rm -rf build or use rimraf) before running your tsc
commands and creating build/esm/package.json so each build starts from a clean
output tree and won't publish leftover artifacts.
In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts`:
- Around line 404-418: The email path returns early using emailCTATemplate but
doesn't sanitize dataset.sponsorLabel; move or duplicate the cleaning logic so
sponsorLabel is sanitized before the email branch executes: when options.target
=== 'email', call buildCleanBasicHtmlForElement(element) and
cleanBasicHtml(dataset.sponsorLabel, {firstChildInnerContent: true}) (same flow
used later), assign the cleaned string back to dataset.sponsorLabel (or a local
sanitized copy) prior to calling
emailCTATemplate/createDocument/renderWithVisibility so the email output uses
the cleaned sponsorLabel.
In `@packages/kg-default-nodes/src/nodes/embed/embed-renderer.ts`:
- Around line 51-55: The thumbnail dimension guard currently checks
thumbnail_width/thumbnail_height truthiness which allows negative values; update
the condition in the isEmail && isVideoWithThumbnail branch to require
metadata.thumbnail_width > 0 and metadata.thumbnail_height > 0 before computing
thumbnailAspectRatio, spacerWidth, and spacerHeight so you never divide by or
produce negative VML sizes (reference variables: metadata.thumbnail_width,
metadata.thumbnail_height, thumbnailAspectRatio, spacerWidth, spacerHeight).
In `@packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts`:
- Around line 47-49: The payload construction uses
readCaptionFromElement(domNode) which can return undefined, causing later
concatenation in gallery-caption-building logic (see readCaptionFromElement and
the payload variable) to produce "undefined / actualCaption"; fix by normalizing
the caption to an empty string (or filtering out falsy values) before storing in
payload.caption so subsequent concatenation or joining code never sees
undefined—update the payload assignment and any concatenation that uses
payload.caption (or siblingCaption assembly logic) to use the normalized string
or skip empty parts.
- Around line 99-110: The current map/filter over gallery images may set img.src
to an empty string when data-src is missing, producing invalid entries for
downstream readGalleryImageAttributesFromElement; update the logic in the
gallery-parser mapping: when img.getAttribute('src') is falsy and
img.previousElementSibling?.tagName === 'NOSCRIPT', attempt to find an <img>
inside that NOSCRIPT and extract its actual src (e.g.,
previousElementSibling.getElementsByTagName('img')[0].getAttribute('src')) and
set that as img.src only if present; if no valid src can be obtained from
data-src or the NOSCRIPT image, return undefined so the subsequent .filter(img
=> img !== undefined) will drop it. Ensure references to img,
previousElementSibling, and readGalleryImageAttributesFromElement remain intact.
In `@packages/kg-default-nodes/src/nodes/html/html-parser.ts`:
- Around line 12-22: The loop in html-parser.ts that collects and removes
siblings (using nextNode/currentNode, currentNode.remove(), (currentNode as
Element).outerHTML and textContent) can run past a missing kg-card-end and
destructively remove unrelated nodes; pre-scan from the starting sibling using
isHtmlEndComment to find the end marker first, and only if an end node is found
perform the existing collection/removal loop; otherwise leave siblings untouched
and skip creating the fused HTML card.
In `@packages/kg-default-nodes/src/nodes/signup/SignupNode.ts`:
- Around line 108-110: The factory $createSignupNode currently accepts a broad
Record<string, unknown>, which bypasses the constructor's stricter SignupData
contract; change the parameter type of $createSignupNode from Record<string,
unknown> to SignupData (importing SignupData if needed) so it matches
SignupNode's constructor signature, update any call sites/tests that pass loose
objects to satisfy the stronger type, and run a repo-wide grep for other
$create*Node factory signatures to make similar fixes for consistency.
In `@packages/kg-default-nodes/test/nodes/header.test.ts`:
- Around line 151-160: The tests currently end with "void element" so they don't
assert anything; update the two "renders nothing" cases (the one using
$createHeaderNode and the duplicate at lines ~468-477) to explicitly assert the
current renderer behavior instead of no-oping: call node.exportDOM(exportOptions
as unknown as LexicalEditor) and then assert that the returned element is not
null and matches the expected shape (e.g., is an Element/HTMLElement and either
has no children or the expected empty structure) so the test actually verifies
the outcome of exportDOM; use the $createHeaderNode,
node.header/node.subheader/buttonEnabled, exportOptions and exportDOM symbols to
locate and modify the assertions.
---
Nitpick comments:
In `@packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts`:
- Around line 50-51: The BookmarkNode constructor currently calls super({}, key)
even though BookmarkNode manually initializes all properties; change the
constructor to call super(key) instead so the base class receives the intended
key parameter and the unused empty props object is removed—update the
BookmarkNode constructor (constructor({url, metadata, caption}: BookmarkData =
{}, key?: string)) to pass only key to super and leave the existing manual
initialization of url, metadata, caption intact.
- Around line 114-116: The factory $createBookmarkNode currently types its
parameter as Record<string, unknown> but constructs a new BookmarkNode which
expects BookmarkData; update the factory signature to accept BookmarkData (or
make BookmarkNode constructor accept Record<string, unknown>) so types
align—specifically change the $createBookmarkNode parameter type to BookmarkData
to match the BookmarkNode constructor and ensure callers pass the correct shape.
In `@packages/kg-default-nodes/src/nodes/callout/callout-parser.ts`:
- Around line 7-25: The constructor signature for CalloutNode in
parseCalloutNode is too loose and forces a runtime cast to LexicalNode; change
the parameter type from "new (data: Record<string, unknown>) => unknown" to "new
(data: Record<string, unknown>) => LexicalNode" so the created instance is
statically known to be a LexicalNode and remove the "as LexicalNode" cast; apply
the same tightening to other parser functions that construct nodes produced by
generateDecoratorNode / KoenigDecoratorNode / DecoratorNode so all constructors
return LexicalNode types.
In `@packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts`:
- Around line 27-28: The CalloutNode constructor currently calls super({}, key)
which is inconsistent with the base class constructor pattern in
generate-decorator-node.ts (constructor(data: Record<string, unknown> = {},
key?: string) that itself calls super(key)); update the CalloutNode constructor
(the constructor({calloutText, calloutEmoji, backgroundColor}: CalloutData = {},
key?: string) in CalloutNode.ts) to call super(key) instead of super({}, key) so
you only forward the key to the base class while still initializing calloutText,
calloutEmoji and backgroundColor locally.
In `@packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts`:
- Around line 22-25: The createElement signature on the BrowserDocument
interface currently returns a broad intersection that makes every element look
like both a canvas and a script; update BrowserDocument.createElement to use
overloads (or a discriminated union) so calling createElement('canvas') returns
a canvas-like BrowserElement with canvas-specific members (width, height,
getContext(...), getImageData(...)) and createElement('script') returns a
script-like BrowserElement with currentScript/parentElement and innerHTML,
keeping the BrowserElement base separate; change the type for createElement in
the BrowserDocument interface and adjust any dependent code to rely on the
specific overloads rather than the combined intersection.
In `@packages/kg-default-nodes/test/nodes/email-cta.test.ts`:
- Around line 213-214: Remove the incorrect cast of the options object to
LexicalEditor when calling emailNode.exportDOM; exportDOM expects a
Record<string, unknown> so stop casting {...exportOptions, ...options} as
unknown as LexicalEditor and pass the merged object directly, and instead
assert/narrow the return using the known shape ({ element: Element | null;
type?: string }) by keeping the subsequent `as HTMLElement` cast on the
`element` variable; update all similar call sites (the ones around lines
referenced) to call exportDOM(...) without the LexicalEditor cast and rely on
the exportDOM return type assertion.
In `@packages/kg-default-nodes/test/nodes/horizontalrule.test.ts`:
- Around line 47-49: The test currently casts the exportOptions argument to
LexicalEditor when calling hrNode.exportDOM; instead, cast the return value of
hrNode.exportDOM to the expected type (e.g., { element } as unknown as {
element: HTMLElement }) to match other tests, remove the unnecessary (element as
HTMLElement) cast on the next line, and ensure the constructed element variable
is used directly in the prettify assertion; reference hrNode.exportDOM,
exportOptions, and element to locate and update the code.
In `@packages/kg-default-nodes/test/nodes/video.test.ts`:
- Around line 245-247: The tests are using an incorrect cast by calling
exportDOM(exportOptions as unknown as LexicalEditor); remove the bogus
LexicalEditor cast and pass exportOptions directly to exportDOM since the method
signature is exportDOM(options: Record<string, unknown>); update all occurrences
(e.g., calls on videoNode.exportDOM and related nodes at the locations flagged)
to call exportDOM(exportOptions) so the real types are used and any underlying
type issues surface.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 2751eb43-7ca8-4957-a935-5db41ba66289
⛔ Files ignored due to path filters (1)
yarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (149)
packages/kg-default-nodes/eslint.config.mjspackages/kg-default-nodes/index.jspackages/kg-default-nodes/package.jsonpackages/kg-default-nodes/rollup.config.mjspackages/kg-default-nodes/src/KoenigDecoratorNode.tspackages/kg-default-nodes/src/generate-decorator-node.tspackages/kg-default-nodes/src/index.tspackages/kg-default-nodes/src/kg-default-nodes.tspackages/kg-default-nodes/src/nodes/ExtendedHeadingNode.tspackages/kg-default-nodes/src/nodes/ExtendedQuoteNode.tspackages/kg-default-nodes/src/nodes/ExtendedTextNode.tspackages/kg-default-nodes/src/nodes/TKNode.tspackages/kg-default-nodes/src/nodes/aside/AsideNode.tspackages/kg-default-nodes/src/nodes/aside/AsideParser.tspackages/kg-default-nodes/src/nodes/at-link/AtLinkNode.tspackages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.tspackages/kg-default-nodes/src/nodes/at-link/index.tspackages/kg-default-nodes/src/nodes/audio/AudioNode.tspackages/kg-default-nodes/src/nodes/audio/audio-parser.tspackages/kg-default-nodes/src/nodes/audio/audio-renderer.tspackages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.tspackages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.tspackages/kg-default-nodes/src/nodes/bookmark/bookmark-renderer.tspackages/kg-default-nodes/src/nodes/button/ButtonNode.tspackages/kg-default-nodes/src/nodes/button/button-parser.tspackages/kg-default-nodes/src/nodes/button/button-renderer.tspackages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.tspackages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.tspackages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.tspackages/kg-default-nodes/src/nodes/callout/CalloutNode.tspackages/kg-default-nodes/src/nodes/callout/callout-parser.tspackages/kg-default-nodes/src/nodes/callout/callout-renderer.tspackages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.tspackages/kg-default-nodes/src/nodes/codeblock/codeblock-parser.tspackages/kg-default-nodes/src/nodes/codeblock/codeblock-renderer.tspackages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.tspackages/kg-default-nodes/src/nodes/email-cta/email-cta-renderer.tspackages/kg-default-nodes/src/nodes/email/EmailNode.tspackages/kg-default-nodes/src/nodes/email/email-renderer.tspackages/kg-default-nodes/src/nodes/embed/EmbedNode.tspackages/kg-default-nodes/src/nodes/embed/embed-parser.tspackages/kg-default-nodes/src/nodes/embed/embed-renderer.tspackages/kg-default-nodes/src/nodes/embed/types/twitter.tspackages/kg-default-nodes/src/nodes/file/FileNode.tspackages/kg-default-nodes/src/nodes/file/file-parser.tspackages/kg-default-nodes/src/nodes/file/file-renderer.tspackages/kg-default-nodes/src/nodes/gallery/GalleryNode.tspackages/kg-default-nodes/src/nodes/gallery/gallery-parser.tspackages/kg-default-nodes/src/nodes/gallery/gallery-renderer.tspackages/kg-default-nodes/src/nodes/header/HeaderNode.tspackages/kg-default-nodes/src/nodes/header/parsers/header-parser.tspackages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.tspackages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.tspackages/kg-default-nodes/src/nodes/horizontalrule/HorizontalRuleNode.tspackages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-parser.tspackages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-renderer.tspackages/kg-default-nodes/src/nodes/html/HtmlNode.tspackages/kg-default-nodes/src/nodes/html/html-parser.tspackages/kg-default-nodes/src/nodes/html/html-renderer.tspackages/kg-default-nodes/src/nodes/image/ImageNode.tspackages/kg-default-nodes/src/nodes/image/image-parser.tspackages/kg-default-nodes/src/nodes/image/image-renderer.tspackages/kg-default-nodes/src/nodes/markdown/MarkdownNode.tspackages/kg-default-nodes/src/nodes/markdown/markdown-renderer.tspackages/kg-default-nodes/src/nodes/paywall/PaywallNode.tspackages/kg-default-nodes/src/nodes/paywall/paywall-parser.tspackages/kg-default-nodes/src/nodes/paywall/paywall-renderer.tspackages/kg-default-nodes/src/nodes/product/ProductNode.tspackages/kg-default-nodes/src/nodes/product/product-parser.tspackages/kg-default-nodes/src/nodes/product/product-renderer.tspackages/kg-default-nodes/src/nodes/signup/SignupNode.tspackages/kg-default-nodes/src/nodes/signup/signup-parser.tspackages/kg-default-nodes/src/nodes/signup/signup-renderer.tspackages/kg-default-nodes/src/nodes/toggle/ToggleNode.tspackages/kg-default-nodes/src/nodes/toggle/toggle-parser.tspackages/kg-default-nodes/src/nodes/toggle/toggle-renderer.tspackages/kg-default-nodes/src/nodes/transistor/TransistorNode.tspackages/kg-default-nodes/src/nodes/transistor/transistor-renderer.tspackages/kg-default-nodes/src/nodes/video/VideoNode.tspackages/kg-default-nodes/src/nodes/video/video-parser.tspackages/kg-default-nodes/src/nodes/video/video-renderer.tspackages/kg-default-nodes/src/nodes/zwnj/ZWNJNode.tspackages/kg-default-nodes/src/serializers/linebreak.tspackages/kg-default-nodes/src/serializers/paragraph.tspackages/kg-default-nodes/src/svg.d.tspackages/kg-default-nodes/src/utils/add-create-document-option.tspackages/kg-default-nodes/src/utils/build-clean-basic-html-for-element.tspackages/kg-default-nodes/src/utils/clean-dom.tspackages/kg-default-nodes/src/utils/escape-html.tspackages/kg-default-nodes/src/utils/get-available-image-widths.tspackages/kg-default-nodes/src/utils/get-resized-image-dimensions.tspackages/kg-default-nodes/src/utils/is-local-content-image.tspackages/kg-default-nodes/src/utils/is-unsplash-image.tspackages/kg-default-nodes/src/utils/read-caption-from-element.tspackages/kg-default-nodes/src/utils/read-image-attributes-from-element.tspackages/kg-default-nodes/src/utils/read-text-content.tspackages/kg-default-nodes/src/utils/render-empty-container.tspackages/kg-default-nodes/src/utils/render-helpers/email-button.tspackages/kg-default-nodes/src/utils/replacement-strings.tspackages/kg-default-nodes/src/utils/rgb-to-hex.tspackages/kg-default-nodes/src/utils/set-src-background-from-parent.tspackages/kg-default-nodes/src/utils/size-byte-converter.tspackages/kg-default-nodes/src/utils/slugify.tspackages/kg-default-nodes/src/utils/srcset-attribute.tspackages/kg-default-nodes/src/utils/tagged-template-fns.tspackages/kg-default-nodes/src/utils/truncate.tspackages/kg-default-nodes/src/utils/visibility.tspackages/kg-default-nodes/test/generate-decorator-node.test.tspackages/kg-default-nodes/test/nodes/aside.test.tspackages/kg-default-nodes/test/nodes/at-link-search.test.tspackages/kg-default-nodes/test/nodes/at-link.test.tspackages/kg-default-nodes/test/nodes/audio.test.tspackages/kg-default-nodes/test/nodes/bookmark.test.tspackages/kg-default-nodes/test/nodes/button.test.tspackages/kg-default-nodes/test/nodes/call-to-action.test.tspackages/kg-default-nodes/test/nodes/callout.test.tspackages/kg-default-nodes/test/nodes/codeblock.test.tspackages/kg-default-nodes/test/nodes/email-cta.test.tspackages/kg-default-nodes/test/nodes/email.test.tspackages/kg-default-nodes/test/nodes/embed.test.tspackages/kg-default-nodes/test/nodes/file.test.tspackages/kg-default-nodes/test/nodes/gallery.test.tspackages/kg-default-nodes/test/nodes/header.test.tspackages/kg-default-nodes/test/nodes/horizontalrule.test.tspackages/kg-default-nodes/test/nodes/html.test.tspackages/kg-default-nodes/test/nodes/image.test.tspackages/kg-default-nodes/test/nodes/markdown.test.tspackages/kg-default-nodes/test/nodes/paywall.test.tspackages/kg-default-nodes/test/nodes/product.test.tspackages/kg-default-nodes/test/nodes/signup.test.tspackages/kg-default-nodes/test/nodes/tk.test.tspackages/kg-default-nodes/test/nodes/toggle.test.tspackages/kg-default-nodes/test/nodes/transistor.test.tspackages/kg-default-nodes/test/nodes/video.test.tspackages/kg-default-nodes/test/nodes/zwnj.test.tspackages/kg-default-nodes/test/serializers/linebreak.test.tspackages/kg-default-nodes/test/serializers/paragraph.test.tspackages/kg-default-nodes/test/test-utils/assertions.tspackages/kg-default-nodes/test/test-utils/html-minifier.d.tspackages/kg-default-nodes/test/test-utils/index.tspackages/kg-default-nodes/test/test-utils/overrides.tspackages/kg-default-nodes/test/test-utils/should-assertions.d.tspackages/kg-default-nodes/test/test-utils/should.d.tspackages/kg-default-nodes/test/utils/rgb-to-hex.test.tspackages/kg-default-nodes/test/utils/tagged-template-fns.test.tspackages/kg-default-nodes/test/utils/visibility.test.tspackages/kg-default-nodes/tsconfig.cjs.jsonpackages/kg-default-nodes/tsconfig.jsonpackages/kg-default-nodes/tsconfig.test.json
💤 Files with no reviewable changes (2)
- packages/kg-default-nodes/index.js
- packages/kg-default-nodes/rollup.config.mjs
✅ Files skipped from review due to trivial changes (30)
- packages/kg-default-nodes/src/index.ts
- packages/kg-default-nodes/src/nodes/at-link/index.ts
- packages/kg-default-nodes/src/svg.d.ts
- packages/kg-default-nodes/src/utils/render-empty-container.ts
- packages/kg-default-nodes/src/utils/escape-html.ts
- packages/kg-default-nodes/src/utils/render-helpers/email-button.ts
- packages/kg-default-nodes/test/utils/tagged-template-fns.test.ts
- packages/kg-default-nodes/src/KoenigDecoratorNode.ts
- packages/kg-default-nodes/src/utils/get-available-image-widths.ts
- packages/kg-default-nodes/src/utils/is-local-content-image.ts
- packages/kg-default-nodes/src/utils/slugify.ts
- packages/kg-default-nodes/test/utils/rgb-to-hex.test.ts
- packages/kg-default-nodes/test/test-utils/should-assertions.d.ts
- packages/kg-default-nodes/tsconfig.test.json
- packages/kg-default-nodes/src/nodes/zwnj/ZWNJNode.ts
- packages/kg-default-nodes/src/serializers/paragraph.ts
- packages/kg-default-nodes/src/utils/build-clean-basic-html-for-element.ts
- packages/kg-default-nodes/src/utils/clean-dom.ts
- packages/kg-default-nodes/tsconfig.cjs.json
- packages/kg-default-nodes/test/serializers/linebreak.test.ts
- packages/kg-default-nodes/test/test-utils/html-minifier.d.ts
- packages/kg-default-nodes/test/test-utils/index.ts
- packages/kg-default-nodes/tsconfig.json
- packages/kg-default-nodes/test/nodes/bookmark.test.ts
- packages/kg-default-nodes/test/nodes/embed.test.ts
- packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-parser.ts
- packages/kg-default-nodes/src/utils/truncate.ts
- packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-renderer.ts
- packages/kg-default-nodes/src/nodes/paywall/PaywallNode.ts
- packages/kg-default-nodes/test/nodes/transistor.test.ts
🚧 Files skipped from review as they are similar to previous changes (69)
- packages/kg-default-nodes/src/serializers/linebreak.ts
- packages/kg-default-nodes/src/utils/get-resized-image-dimensions.ts
- packages/kg-default-nodes/src/utils/is-unsplash-image.ts
- packages/kg-default-nodes/src/utils/read-caption-from-element.ts
- packages/kg-default-nodes/src/nodes/horizontalrule/HorizontalRuleNode.ts
- packages/kg-default-nodes/src/nodes/email/EmailNode.ts
- packages/kg-default-nodes/src/nodes/file/file-parser.ts
- packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts
- packages/kg-default-nodes/src/nodes/aside/AsideParser.ts
- packages/kg-default-nodes/src/nodes/button/button-parser.ts
- packages/kg-default-nodes/src/nodes/button/button-renderer.ts
- packages/kg-default-nodes/src/utils/read-text-content.ts
- packages/kg-default-nodes/src/utils/add-create-document-option.ts
- packages/kg-default-nodes/src/utils/replacement-strings.ts
- packages/kg-default-nodes/src/utils/rgb-to-hex.ts
- packages/kg-default-nodes/test/nodes/aside.test.ts
- packages/kg-default-nodes/test/test-utils/should.d.ts
- packages/kg-default-nodes/src/nodes/email/email-renderer.ts
- packages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.ts
- packages/kg-default-nodes/src/nodes/file/FileNode.ts
- packages/kg-default-nodes/src/nodes/image/image-parser.ts
- packages/kg-default-nodes/src/nodes/paywall/paywall-parser.ts
- packages/kg-default-nodes/src/nodes/paywall/paywall-renderer.ts
- packages/kg-default-nodes/src/nodes/email-cta/email-cta-renderer.ts
- packages/kg-default-nodes/test/nodes/button.test.ts
- packages/kg-default-nodes/test/nodes/zwnj.test.ts
- packages/kg-default-nodes/test/test-utils/overrides.ts
- packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts
- packages/kg-default-nodes/src/nodes/product/ProductNode.ts
- packages/kg-default-nodes/src/nodes/toggle/ToggleNode.ts
- packages/kg-default-nodes/src/nodes/header/parsers/header-parser.ts
- packages/kg-default-nodes/src/nodes/toggle/toggle-parser.ts
- packages/kg-default-nodes/src/nodes/ExtendedQuoteNode.ts
- packages/kg-default-nodes/test/nodes/email.test.ts
- packages/kg-default-nodes/test/nodes/image.test.ts
- packages/kg-default-nodes/test/nodes/signup.test.ts
- packages/kg-default-nodes/test/nodes/markdown.test.ts
- packages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.ts
- packages/kg-default-nodes/src/nodes/header/renderers/v1/header-renderer.ts
- packages/kg-default-nodes/src/nodes/product/product-parser.ts
- packages/kg-default-nodes/src/utils/size-byte-converter.ts
- packages/kg-default-nodes/test/nodes/paywall.test.ts
- packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts
- packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-parser.ts
- packages/kg-default-nodes/src/nodes/image/ImageNode.ts
- packages/kg-default-nodes/src/utils/tagged-template-fns.ts
- packages/kg-default-nodes/test/nodes/gallery.test.ts
- packages/kg-default-nodes/src/nodes/signup/signup-parser.ts
- packages/kg-default-nodes/test/nodes/tk.test.ts
- packages/kg-default-nodes/test/serializers/paragraph.test.ts
- packages/kg-default-nodes/src/nodes/image/image-renderer.ts
- packages/kg-default-nodes/test/nodes/html.test.ts
- packages/kg-default-nodes/test/nodes/audio.test.ts
- packages/kg-default-nodes/src/nodes/html/HtmlNode.ts
- packages/kg-default-nodes/test/nodes/at-link-search.test.ts
- packages/kg-default-nodes/src/nodes/video/VideoNode.ts
- packages/kg-default-nodes/src/utils/srcset-attribute.ts
- packages/kg-default-nodes/src/nodes/signup/signup-renderer.ts
- packages/kg-default-nodes/src/nodes/codeblock/codeblock-parser.ts
- packages/kg-default-nodes/src/nodes/file/file-renderer.ts
- packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts
- packages/kg-default-nodes/src/nodes/ExtendedTextNode.ts
- packages/kg-default-nodes/src/nodes/aside/AsideNode.ts
- packages/kg-default-nodes/src/nodes/transistor/transistor-renderer.ts
- packages/kg-default-nodes/src/nodes/video/video-renderer.ts
- packages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.ts
- packages/kg-default-nodes/test/nodes/call-to-action.test.ts
- packages/kg-default-nodes/test/nodes/codeblock.test.ts
- packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts
| // should not render anything - this is a placeholder node | ||
| exportDOM() { | ||
| return null; | ||
| return {element: null}; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "AtLinkNode exportDOM():"
sed -n '55,70p' packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts
echo
echo "Related AtLinkSearchNode exportDOM():"
sed -n '60,75p' packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts
echo
echo "HTML renderer caller:"
sed -n '32,60p' packages/kg-lexical-html-renderer/lib/convert-to-html-string.ts
echo
echo "Declared kg-default-nodes exportDOM contract:"
sed -n '12,24p' packages/kg-lexical-html-renderer/lib/kg-default-nodes.d.tsRepository: TryGhost/Koenig
Length of output: 2658
Return a real DOM export payload here.
exportDOM() returns {element: null}, which violates the declared type contract (element: HTMLElement | HTMLInputElement | HTMLTextAreaElement). The HTML renderer in convert-to-html-string.ts calls element.innerHTML, element.outerHTML, or checks properties on element without null guards—if this node reaches that code path, it crashes with TypeError: Cannot read property of null.
The same issue exists in AtLinkSearchNode.
🛠️ Safer fallback
// should not render anything - this is a placeholder node
exportDOM() {
- return {element: null};
+ return {
+ element: document.createElement('span'),
+ type: 'outer'
+ };
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // should not render anything - this is a placeholder node | |
| exportDOM() { | |
| return null; | |
| return {element: null}; | |
| // should not render anything - this is a placeholder node | |
| exportDOM() { | |
| return { | |
| element: document.createElement('span'), | |
| type: 'outer' | |
| }; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts` around lines 59 -
61, exportDOM() currently returns {element: null} in AtLinkNode (and similarly
in AtLinkSearchNode), which violates the expected return type (HTMLElement |
HTMLInputElement | HTMLTextAreaElement) and causes consumers to dereference a
null. Update exportDOM in both AtLinkNode and AtLinkSearchNode to return a real
HTMLElement fallback (e.g., create a span or input element via
document.createElement and set any needed attributes or text/ARIA/data-
attributes to represent the placeholder) so callers like
convert-to-html-string.ts can safely access element.innerHTML/outerHTML without
null checks; ensure the returned object shape matches the declared contract.
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/kg-default-nodes/src/utils/get-resized-image-dimensions.ts (1)
6-12:⚠️ Potential issue | 🟡 MinorGuard invalid dimensions before ratio calculation.
Line 8 can produce
Infinity/NaNwhenimage.height <= 0(or invalid width), which can leak invalid resized values into HTML attributes.Suggested fix
export const getResizedImageDimensions = function (image: ImageDimensions, {width: desiredWidth, height: desiredHeight}: {width?: number; height?: number} = {}): ImageDimensions { const {width, height} = image; + if (width <= 0 || height <= 0) { + return {width, height}; + } const ratio = width / height;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/utils/get-resized-image-dimensions.ts` around lines 6 - 12, The function getResizedImageDimensions currently computes ratio = width / height before validating image dimensions, which can produce NaN/Infinity when image.width or image.height are non-finite or <= 0; add a guard at the top that checks Number.isFinite(image.width) && Number.isFinite(image.height) && image.width > 0 && image.height > 0, and if the check fails, avoid computing ratio and simply return the original image (or a safe fallback) so resizedHeight/width calculations using ratio (and the variables width, height, desiredWidth, desiredHeight) cannot produce invalid values for HTML attributes.
♻️ Duplicate comments (7)
packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts (1)
43-45:⚠️ Potential issue | 🟡 MinorAlign
$createCalloutNode()parameter type with constructor.The constructor accepts
CalloutData, but the factory function parameter is typed asRecord<string, unknown>. This forces callers to cast and loses type safety benefits of theCalloutDatainterface.♻️ Suggested fix
-export const $createCalloutNode = (dataset: Record<string, unknown>) => { +export const $createCalloutNode = (dataset: CalloutData = {}) => { return new CalloutNode(dataset); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts` around lines 43 - 45, The factory $createCalloutNode currently types its parameter as Record<string, unknown> which conflicts with the CalloutNode constructor that expects CalloutData; change the parameter type of $createCalloutNode to CalloutData so callers get proper type safety and avoid casts—update the signature of $createCalloutNode(dataset: CalloutData) and ensure CalloutData is imported/available in the module alongside the CalloutNode reference.packages/kg-default-nodes/src/nodes/html/html-parser.ts (1)
12-22:⚠️ Potential issue | 🟠 MajorGuard against missing
kg-card-endbefore destructive removal.If the end comment marker is absent, this loop removes all following siblings and folds unrelated content into one HTML card. Consider pre-scanning for the matching end comment before mutating/removing nodes.
🛡️ Suggested fix
conversion(domNode: Node) { const html = []; let nextNode = domNode.nextSibling; + + // Pre-scan for end marker + let endNode = nextNode; + while (endNode && !isHtmlEndComment(endNode)) { + endNode = endNode.nextSibling; + } + + if (!endNode) { + return null; + } - while (nextNode && !isHtmlEndComment(nextNode)) { + while (nextNode && nextNode !== endNode) { const currentNode = nextNode; nextNode = currentNode.nextSibling; if (currentNode.nodeType === 1) { html.push((currentNode as Element).outerHTML); } else if (currentNode.nodeType === 3 && currentNode.textContent) { html.push(currentNode.textContent); } // remove nodes as we go so that they don't go through the parser currentNode.remove(); } + endNode.remove();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/html/html-parser.ts` around lines 12 - 22, The loop that removes siblings using isHtmlEndComment can destructively remove unrelated content if a matching `kg-card-end` comment is missing; before mutating nodes in the while loop (where `nextNode`, `currentNode`, and `html` are used), pre-scan the sibling chain for a `isHtmlEndComment` match and only run the removal/aggregation if a matching end comment is found; if not found, abort the operation (skip building the card or return early) to avoid removing nodes.packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts (1)
11-16:⚠️ Potential issue | 🟠 MajorGuard email rendering when
postUrlis missing.Line 15 keeps
postUrloptional, but Lines 192/201/205/213/216 require it for email links. This can renderhref="undefined"whentarget === 'email'.Proposed fix
export function renderAudioNode(node: AudioNodeData, options: RenderOptions = {}) { addCreateDocumentOption(options); const document = options.createDocument!(); if (!node.src || node.src.trim() === '') { return renderEmptyContainer(document); } + + if (options.target === 'email' && (!options.postUrl || options.postUrl.trim() === '')) { + return renderEmptyContainer(document); + } const thumbnailCls = getThumbnailCls(node); const emptyThumbnailCls = getEmptyThumbnailCls(node); if (options.target === 'email') {Also applies to: 30-31, 184-216
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/audio/audio-renderer.ts` around lines 11 - 16, RenderOptions.postUrl is optional but code paths that render email links when target === 'email' assume it exists and can produce href="undefined"; update the email link rendering branches in audio-renderer.ts (the code that checks target === 'email' around the rendering logic lines ~184-216) to first verify options.postUrl is a non-empty string and only construct an anchor href using postUrl when present; if postUrl is missing, either skip creating the anchor (render plain text/disabled link) or fall back to a safe default (e.g., omit href) so no href="undefined" is emitted. Ensure all spots that build email hrefs reference RenderOptions.postUrl and perform the guard.packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts (1)
404-419:⚠️ Potential issue | 🟠 MajorSanitize
sponsorLabelbefore the email early return.Line 404 returns through the email path before Line 415 sanitization runs, so
dataset.sponsorLabelis still raw when interpolated intoemailCTATemplate(...).Suggested fix
export function renderCallToActionNode(node: CTANodeData, options: CTARenderOptions = {}) { addCreateDocumentOption(options); const document = options.createDocument!(); const dataset = { @@ linkColor: node.linkColor }; + if (dataset.hasSponsorLabel) { + const cleanBasicHtml = buildCleanBasicHtmlForElement(document.createElement('div')); + const cleanedHtml = cleanBasicHtml(dataset.sponsorLabel, {firstChildInnerContent: true}); + dataset.sponsorLabel = cleanedHtml || ''; + } + // Add validation for backgroundColor @@ if (options.target === 'email') { const emailDoc = options.createDocument!(); const emailDiv = emailDoc.createElement('div'); @@ const element = document.createElement('div'); - - if (dataset.hasSponsorLabel) { - const cleanBasicHtml = buildCleanBasicHtmlForElement(element); - const cleanedHtml = cleanBasicHtml(dataset.sponsorLabel, {firstChildInnerContent: true}); - dataset.sponsorLabel = cleanedHtml || ''; - }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/call-to-action/calltoaction-renderer.ts` around lines 404 - 419, The email branch returns early before sanitizing dataset.sponsorLabel so raw HTML can be injected into emailCTATemplate; move or duplicate the sanitization logic using buildCleanBasicHtmlForElement(...) and cleanBasicHtml(...) to run before the options.target === 'email' return (or sanitize unconditionally at the top of the function), ensure you update dataset.sponsorLabel = cleanedHtml || '' prior to calling emailCTATemplate(...) and then continue to call renderWithVisibility(..., node.visibility, options) as before.packages/kg-default-nodes/src/nodes/video/video-renderer.ts (2)
27-32:⚠️ Potential issue | 🟠 Major
target: 'email'still isn't safely enforced withpostUrl.Current union + cast still permits an email path with missing
postUrlat compile time (target?: stringin default options plusas EmailVideoRenderOptions). This can still render broken email links.🔧 Suggested fix (type guard + no unsafe cast)
type VideoRenderOptions = EmailVideoRenderOptions | DefaultVideoRenderOptions; + +function isEmailRenderOptions(options: VideoRenderOptions): options is EmailVideoRenderOptions { + return options.target === 'email' && typeof options.postUrl === 'string' && options.postUrl.trim() !== ''; +} @@ - const htmlString = options.target === 'email' - ? emailCardTemplate({node, options: options as EmailVideoRenderOptions, cardClasses}) - : cardTemplate({node, cardClasses}); + const htmlString = isEmailRenderOptions(options) + ? emailCardTemplate({node, options, cardClasses}) + : cardTemplate({node, cardClasses});#!/bin/bash set -euo pipefail # Verify that email branch currently depends on a cast instead of a safe narrowing guard rg -n "interface DefaultVideoRenderOptions|target\\?: string|postUrl\\?: string|options as EmailVideoRenderOptions" \ packages/kg-default-nodes/src/nodes/video/video-renderer.ts # Optional call-site audit: email-target render calls should all include postUrl ast-grep --lang ts --pattern "renderVideoNode($NODE, {$$$, target: 'email', $$$})" ast-grep --lang ts --pattern "renderVideoNode($NODE, {$$$, target: 'email', $$$, postUrl: $URL, $$$})"Also applies to: 45-47
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/video/video-renderer.ts` around lines 27 - 32, The union types currently allow an email path without postUrl because DefaultVideoRenderOptions has target?: string and callers cast to EmailVideoRenderOptions; fix by making target a discriminant (e.g., in DefaultVideoRenderOptions set target?: 'default' or remove overlapping target) and remove unsafe casts, then add a type guard function isEmailVideoOptions(options): options is EmailVideoRenderOptions that checks options.target === 'email' and options.postUrl exists; update renderVideoNode and any callers to use this guard before treating options as EmailVideoRenderOptions so the compiler enforces that postUrl is present for the email branch.
4-7:⚠️ Potential issue | 🟠 MajorKeep
width/heightnullable to match the node model.
VideoNodeDatanarrows dimensions tonumber, but downstream logic assumes valid numeric dimensions for spacer URL/aspect math. If upstream node data allows null dimensions, this can produce invalid output (NaN/broken spacer sizing).🔧 Suggested fix
interface VideoNodeData { src: string; - width: number; - height: number; + width: number | null; + height: number | null; caption: string; @@ } + +type SizedVideoNodeData = VideoNodeData & {width: number; height: number}; + +function hasVideoDimensions(node: VideoNodeData): node is SizedVideoNodeData { + return node.width != null && node.height != null && node.height > 0; +} @@ export function renderVideoNode(node: VideoNodeData, options: VideoRenderOptions = {}) { @@ - if (!node.src || node.src.trim() === '') { + if (!node.src || node.src.trim() === '' || !hasVideoDimensions(node)) { return renderEmptyContainer(document); } @@ - ? emailCardTemplate({node, options: options as EmailVideoRenderOptions, cardClasses}) - : cardTemplate({node, cardClasses}); + ? emailCardTemplate({node, options: options as EmailVideoRenderOptions, cardClasses}) + : cardTemplate({node, cardClasses});#!/bin/bash set -euo pipefail # Verify nullable source model vs renderer narrowing rg -n "width: number \\| null|height: number \\| null" packages/kg-default-nodes/src/nodes/video/VideoNode.ts rg -n "interface VideoNodeData|width: number;|height: number;" packages/kg-default-nodes/src/nodes/video/video-renderer.ts rg -n "aspectRatio|spacergif|emailSpacerHeight" packages/kg-default-nodes/src/nodes/video/video-renderer.tsAlso applies to: 126-129
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/video/video-renderer.ts` around lines 4 - 7, VideoNodeData currently declares width and height as number but upstream model allows null; change the VideoNodeData interface to allow width: number | null and height: number | null, then update any downstream calculations in this module that assume numeric dimensions (look for aspectRatio, spacergif, emailSpacerHeight and any spacer URL construction) to guard against nulls — use conditional checks or fallbacks (e.g., skip aspect math or default to 1/0 values) before dividing/multiplying so you never produce NaN or malformed spacer URLs when width/height are null.packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts (1)
39-77:⚠️ Potential issue | 🟡 MinorAdd an explicit
baseSrcguard before fallback assignment and URL construction.Line 39 can still produce
null/empty values, while Lines 66, 72, and 77 rely onbaseSrc!. This can still fail at runtime in edge cases and hides the real precondition.🛠️ Suggested fix
const baseSrc = el.getAttribute('data-src'); + if (!baseSrc) { + return; + } @@ - el.src = baseSrc!; + el.src = baseSrc; return; @@ - el.src = baseSrc!; + el.src = baseSrc; return; @@ - const u = new URL(baseSrc!); + const u = new URL(baseSrc);#!/bin/bash # Verify unresolved non-null assertions and missing guard around baseSrc in this file rg -n -C2 "const baseSrc = el.getAttribute\('data-src'\)|baseSrc!" packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts` around lines 39 - 77, The code uses baseSrc (const baseSrc = el.getAttribute('data-src')) with non-null assertions in multiple places (el.src = baseSrc! and new URL(baseSrc!)) without an explicit guard; add an early check that baseSrc is present and non-empty (e.g., if (!baseSrc) { return; } or set a safe fallback) before any use, and replace the fallback assignments and the new URL(baseSrc!) call in this function (references: baseSrc, colorToRgb, the while loop that searches parent bg, the el.src assignments and new URL(...) call) so runtime errors are avoided when data-src is null/empty. Ensure the guard is placed before the bg-check branch exits and before constructing the URL.
🧹 Nitpick comments (20)
packages/kg-default-nodes/test/serializers/linebreak.test.ts (1)
47-50: Reduce repeatedElementNodecasts in assertions.There are many repeated
(nodes[i] as ElementNode).getChildren()calls. Consider extracting the casted node/children once per block to improve readability and reduce assertion noise.♻️ Example refactor pattern
-should.equal((nodes[0] as ElementNode).getChildren().length, 3); -should.equal((nodes[0] as ElementNode).getChildren()[0].getType(), 'extended-text'); -should.equal((nodes[0] as ElementNode).getChildren()[1].getType(), 'linebreak'); -should.equal((nodes[0] as ElementNode).getChildren()[2].getType(), 'extended-text'); +const paragraphChildren = (nodes[0] as ElementNode).getChildren(); +should.equal(paragraphChildren.length, 3); +should.equal(paragraphChildren[0].getType(), 'extended-text'); +should.equal(paragraphChildren[1].getType(), 'linebreak'); +should.equal(paragraphChildren[2].getType(), 'extended-text');Also applies to: 63-65, 70-72, 82-84, 87-89
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/serializers/linebreak.test.ts` around lines 47 - 50, Extract the casted ElementNode and/or its children into a local variable to avoid repeating `(nodes[i] as ElementNode).getChildren()` in the assertions; for example, create a local `const el = nodes[0] as ElementNode` (or `const children = el.getChildren()`) and then assert on `children.length` and `children[0/1/2].getType()` instead of repeating the cast. Apply the same change for the other blocks referenced (lines with nodes[1], nodes[2], etc.) so each assertion group uses a single `ElementNode`/`children` variable for clarity and reduced duplication.packages/kg-default-nodes/test/nodes/horizontalrule.test.ts (1)
11-13: Remove staledatasetusage and renameasideNodefor clarity.
Line 69mutatesdataset.cardWidth, butdatasetis no longer used by$createHorizontalRuleNode()atLine 71; alsoasideNodeis misleading in a horizontal-rule test.Proposed cleanup
- let dataset: Record<string, unknown>; let exportOptions: {dom: typeof dom}; @@ - dataset = {}; - exportOptions = { dom }; @@ - dataset.cardWidth = 'wide'; - - const asideNode = $createHorizontalRuleNode(); - const json = asideNode.exportJSON(); + const hrNode = $createHorizontalRuleNode(); + const json = hrNode.exportJSON();Also applies to: 69-73
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/horizontalrule.test.ts` around lines 11 - 13, Remove the stale dataset usage and rename the misleading asideNode variable: delete the dataset variable mutation (dataset.cardWidth) and any references to dataset since $createHorizontalRuleNode() no longer consumes it, and rename asideNode to hrNode or horizontalRuleNode in horizontalrule.test.ts (references to asideNode, creation, and assertions) so the test reads clearly and only passes necessary args to $createHorizontalRuleNode().packages/kg-default-nodes/test/nodes/button.test.ts (1)
117-117: Optional: centralize repeatedexportDOMcasting in a small helper.This would reduce repetition and keep test intent clearer without changing behavior.
♻️ Proposed refactor
+ const exportButtonElement = (buttonNode: ButtonNode, options: Record<string, unknown>) => + buttonNode.exportDOM(options as unknown as LexicalEditor) as {element: HTMLElement}; + describe('exportDOM', function () { it('creates a button card', editorTest(function () { const buttonNode = $createButtonNode(dataset); - const {element} = buttonNode.exportDOM(exportOptions as unknown as LexicalEditor) as {element: HTMLElement}; + const {element} = exportButtonElement(buttonNode, exportOptions); @@ - const {element} = buttonNode.exportDOM({...exportOptions, ...options} as unknown as LexicalEditor) as {element: HTMLElement}; + const {element} = exportButtonElement(buttonNode, {...exportOptions, ...options}); @@ - const {element} = buttonNode.exportDOM({...exportOptions, ...options} as unknown as LexicalEditor) as {element: HTMLElement}; + const {element} = exportButtonElement(buttonNode, {...exportOptions, ...options}); @@ - const {element} = buttonNode.exportDOM({...exportOptions, ...options} as unknown as LexicalEditor) as {element: HTMLElement}; + const {element} = exportButtonElement(buttonNode, {...exportOptions, ...options});Also applies to: 127-127, 144-144, 176-176
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/button.test.ts` at line 117, Create a small test helper that centralizes the repeated cast of exportDOM results (currently written as buttonNode.exportDOM(exportOptions as unknown as LexicalEditor) as {element: HTMLElement}) and use it wherever that pattern appears (e.g., the calls around the occurrences referencing buttonNode.exportDOM with exportOptions and the cast to {element: HTMLElement}); implement a function like getExportedElement(node, options) that invokes node.exportDOM(options as unknown as LexicalEditor) and returns the typed {element: HTMLElement}, then replace each inline cast (including the other occurrences noted) with calls to that helper to reduce duplication and make intent clearer.packages/kg-default-nodes/test/nodes/call-to-action.test.ts (1)
718-719: Fix local variable typo (docuement→document).This is harmless at runtime, but it hurts readability and searchability.
✏️ Suggested patch
- const docuement = createDocument(htmlTemplate`${element.outerHTML.toString()}`); - const nodes = $generateNodesFromDOM(editor, docuement) as CallToActionNode[]; + const document = createDocument(htmlTemplate`${element.outerHTML.toString()}`); + const nodes = $generateNodesFromDOM(editor, document) as CallToActionNode[];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/call-to-action.test.ts` around lines 718 - 719, Rename the misspelled local variable "docuement" to "document" where it's assigned via createDocument(htmlTemplate`${element.outerHTML.toString()}`) and subsequently passed into $generateNodesFromDOM(editor, document) as CallToActionNode[] so the code reads const document = createDocument(...); const nodes = $generateNodesFromDOM(editor, document) as CallToActionNode[]; update all occurrences in this test block to use the corrected identifier for readability and searchability.packages/kg-default-nodes/src/nodes/toggle/toggle-parser.ts (1)
3-23: Tighten constructor typing to avoid unnecessaryLexicalNodecast
parseToggleNodeaccepts a constructor returningunknownand then casts the result toLexicalNode. Since all callers pass LexicalNode-compatible classes (e.g.,ToggleNodeextendsGeneratedDecoratorNodeBase→KoenigDecoratorNode→DecoratorNode), the parameter type and cast can be tightened:Proposed change
-export function parseToggleNode(ToggleNode: new (data: Record<string, unknown>) => unknown) { +export function parseToggleNode(ToggleNode: new (data: Record<string, unknown>) => LexicalNode) { ... - const node = new ToggleNode(payload); - return {node: node as LexicalNode}; + const node = new ToggleNode(payload); + return {node};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/toggle/toggle-parser.ts` around lines 3 - 23, The parseToggleNode currently types its ToggleNode parameter as returning unknown and then casts new ToggleNode(payload) to LexicalNode; tighten the constructor signature to reflect that ToggleNode produces a LexicalNode-compatible instance (e.g., change the parameter type from new (data: Record<string, unknown>) => unknown to new (data: Record<string, unknown>) => LexicalNode or the specific node class) and remove the unnecessary cast where the node is returned; update the parseToggleNode signature and any related generics so the created node (via new ToggleNode(payload)) is already correctly typed.packages/kg-default-nodes/test/utils/visibility.test.ts (1)
209-220: Tightentargetparameter to prevent invalid test inputs.
runRenderacceptstarget: string, but the implementation only handles'web'and'email'. Narrowing to'web' | 'email'will catch accidental invalid targets at compile time. All six test cases consistently use only these two values.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/utils/visibility.test.ts` around lines 209 - 220, The runRender helper allows any string for the target but the code and callers only support 'web' and 'email'; tighten its signature by changing the target parameter type to the string literal union 'web' | 'email' so invalid targets are caught at compile time; update the runRender declaration (and any related tests calling it) and keep using buildVisibility and renderWithVisibility as-is to ensure the visibilityWithDefaults and render call remain unchanged.packages/kg-default-nodes/src/nodes/header/parsers/header-parser.ts (1)
3-3: Use recommendedLexicalNodeconstructor typing to eliminate unsafe casts.Line 3 currently types
HeaderNodeas returningunknown, requiring force-casts toLexicalNodeat Lines 40 and 84. Lexical v0.13.1 documentation recommends constraining node constructors tonew (...) => LexicalNodedirectly in conversion callbacks. This removes type unsafety and eliminates unnecessary casts.Suggested change
import type {LexicalNode} from 'lexical'; -export function parseHeaderNode(HeaderNode: new (data: Record<string, unknown>) => unknown) { +export function parseHeaderNode(HeaderNode: new (data: Record<string, unknown>) => LexicalNode) {Also applies to: 40-40, 84-84
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/header/parsers/header-parser.ts` at line 3, The parseHeaderNode function currently types HeaderNode as returning unknown which forces unsafe casts; change the constructor signature in parseHeaderNode to HeaderNode: new (data: Record<string, unknown>) => LexicalNode (importing/using LexicalNode type) and then remove the force-casts where instances are created/returned (the locations around the current casts at the conversions used near the previous lines ~40 and ~84), so the returned instances are already typed as LexicalNode and no "as LexicalNode" casts are needed.packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts (1)
213-215: Refactor to validatecreateDocumentonce instead of using repeated non-null assertions.The helper
addCreateDocumentOption()conditionally setsoptions.createDocumentbut doesn't guarantee it will be non-null—if neithercreateDocumentnordomare provided in options, the assertion will fail at runtime. The function usesoptions.createDocument!()at both lines 215 and 238, making it more robust to capture and validate once upfront.♻️ Proposed refactor
export function renderHeaderNodeV2(dataset: HeaderV2DatasetNode, options: HeaderV2RenderOptions = {}) { addCreateDocumentOption(options); - const document = options.createDocument!(); + const createDocument = options.createDocument; + if (!createDocument) { + throw new Error('renderHeaderNodeV2 requires options.createDocument'); + } + const document = createDocument(); @@ if (options.target === 'email') { - const emailDoc = options.createDocument!(); + const emailDoc = createDocument(); const emailDiv = emailDoc.createElement('div');🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/header/renderers/v2/header-renderer.ts` around lines 213 - 215, renderHeaderNodeV2 calls addCreateDocumentOption then uses options.createDocument!() at multiple sites (lines shown) which can still be undefined and crash; update renderHeaderNodeV2 to capture and validate the createDocument function once after addCreateDocumentOption by assigning const createDocument = options.createDocument and throwing/logging a clear error if it's falsy, then replace subsequent uses of options.createDocument!() with createDocument() to avoid repeated non-null assertions (references: renderHeaderNodeV2, addCreateDocumentOption, options.createDocument).packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts (1)
25-27: Avoid mutatingnodeduring render.Line 26 mutates input state inside the renderer. Use a local sanitized
backgroundColorinstead, so rendering stays side-effect free.Proposed refactor
- if (!node.backgroundColor || !node.backgroundColor.match(/^[a-zA-Z\d-]+$/)) { - node.backgroundColor = 'white'; - } - - element.classList.add('kg-card', 'kg-callout-card', `kg-callout-card-${node.backgroundColor}`); + const backgroundColor = (node.backgroundColor && /^[a-zA-Z\d-]+$/.test(node.backgroundColor)) + ? node.backgroundColor + : 'white'; + + element.classList.add('kg-card', 'kg-callout-card', `kg-callout-card-${backgroundColor}`);Also applies to: 29-29
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/callout/callout-renderer.ts` around lines 25 - 27, The renderer is mutating the input `node` (setting node.backgroundColor) which breaks render side-effect guarantees; instead, compute a local sanitized value (e.g., const backgroundColorSanitized) by validating node.backgroundColor with the existing /^[a-zA-Z\d-]+$/ check and defaulting to 'white' when invalid, then use that local variable in the render output (update usages in the callout renderer where node.backgroundColor is referenced, including the other occurrence noted) so the `node` object is never modified during rendering.packages/kg-default-nodes/test/nodes/email-cta.test.ts (3)
213-217: Extract repeated export/assert setup into one helper.The repeated arrange/act block makes this section harder to maintain. A tiny helper returning
outerHTMLwill remove duplication and keep each case focused on expected output.Also applies to: 240-244, 262-266, 284-288, 306-310, 334-338, 362-366, 402-406, 440-444, 477-481
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/email-cta.test.ts` around lines 213 - 217, Multiple tests repeat the same arrange/act of calling emailNode.exportDOM({...exportOptions, ...options} as unknown as LexicalEditor) and asserting element.outerHTML; extract that into a small helper (e.g. getExportedOuterHTML) that accepts the emailNode plus optional options, calls emailNode.exportDOM({...exportOptions, ...opts} as unknown as LexicalEditor), casts element to HTMLElement and returns element.outerHTML, then replace the repeated blocks in the tests (the blocks around emailNode.exportDOM, exportOptions, options, LexicalEditor and element.outerHTML) with calls to this helper so each test only asserts the expected HTML.
11-12: Use concrete fixture types instead ofRecord<string, unknown>.
datasetandexportOptionsare broad enough to hide field typos and wrong value types in tests. A small local type keeps strict mode useful here.♻️ Suggested typing refinement
+type EmailCtaDataset = { + alignment: string; + buttonText: string; + buttonUrl: string; + html: string; + segment: string; + showButton: boolean; + showDividers: boolean; +}; + +type EmailExportOptions = { + exportFormat: 'html'; + dom: typeof dom; +}; + let editor: LexicalEditor; -let dataset: Record<string, unknown>; -let exportOptions: Record<string, unknown>; +let dataset: EmailCtaDataset; +let exportOptions: EmailExportOptions;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/email-cta.test.ts` around lines 11 - 12, Replace the overly-broad types for the test fixtures by declaring small concrete interfaces/types for dataset and exportOptions instead of Record<string, unknown>; locate the top-level variables dataset and exportOptions in packages/kg-default-nodes/test/nodes/email-cta.test.ts, define a minimal FixtureDataset (e.g., fields used by the tests like subject, body, recipients, etc.) and ExportOptions (e.g., format, includeMetadata, locale) that match actual usages in the tests, then update the dataset and exportOptions declarations and any fixture values to use those types so TypeScript catches typos and incorrect value shapes.
179-179: Avoid blind cast when reading root children.Casting children directly to
EmailCtaNode[]can mask an unexpected node type. Assert first, then narrow.✅ Safer narrowing in test
-const [emailNode] = $getRoot().getChildren() as EmailCtaNode[]; +const [firstChild] = $getRoot().getChildren(); +$isEmailCtaNode(firstChild).should.be.true(); +const emailNode = firstChild as EmailCtaNode;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/email-cta.test.ts` at line 179, The test is blindly casting root children to EmailCtaNode[] which can hide wrong node types; instead read children via $getRoot().getChildren(), assert the child exists and is an EmailCtaNode (e.g., via a runtime type check, instanceof, or node.type property), then narrow to EmailCtaNode before assigning to emailNode (use Array.prototype.find/filter to locate the EmailCtaNode and add an expect/assert to fail the test if not found) so that EmailCtaNode is only used after a safe runtime assertion.packages/kg-default-nodes/src/nodes/signup/signup-parser.ts (1)
16-16: Tighten constructor typing to remove the unsafeLexicalNodecast.At line 16, the constructor parameter is typed to return
unknown, forcing an unsafe cast at line 62. SinceSignupNodeextends the result ofgenerateDecoratorNode()(which extendsKoenigDecoratorNode→DecoratorNodefrom lexical), instances are guaranteed to beLexicalNode. Change the constructor return type toLexicalNodeto let TypeScript enforce this at compile time and eliminate the cast.Proposed diff
-export function signupParser(SignupNode: new (data: Record<string, unknown>) => unknown) { +export function signupParser(SignupNode: new (data: Record<string, unknown>) => LexicalNode) { @@ - const node = new SignupNode(payload); - return {node: node as LexicalNode}; + const node = new SignupNode(payload); + return {node};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/signup/signup-parser.ts` at line 16, The constructor parameter of signupParser is currently typed to return unknown, forcing an unsafe cast elsewhere; change the SignupNode constructor signature in signupParser to return LexicalNode (not unknown) so TypeScript knows instances are LexicalNode (since SignupNode extends the result of generateDecoratorNode(), which extends KoenigDecoratorNode/DecoratorNode), then remove the unsafe cast where SignupNode is instantiated (the cast to LexicalNode) and rely on the tightened type to satisfy the compiler; update the type import if necessary to reference LexicalNode used by generateDecoratorNode()/KoenigDecoratorNode.packages/kg-default-nodes/test/nodes/file.test.ts (3)
121-123: Assertimgexistence before casting.The direct cast can hide null until property access; an existence assertion gives cleaner failure output in test runs.
♻️ Proposed refactor
- const icon = el.querySelector('img') as HTMLImageElement; - icon.src.should.equal('https://static.ghost.org/v4.0.0/images/download-icon-darkmode.png'); - icon.style.height.should.equal('24px'); + const icon = el.querySelector('img'); + should.exist(icon); + (icon as HTMLImageElement).src.should.equal('https://static.ghost.org/v4.0.0/images/download-icon-darkmode.png'); + (icon as HTMLImageElement).style.height.should.equal('24px'); ... - const icon = el.querySelector('img') as HTMLImageElement; - icon.style.height.should.equal('20px'); + const icon = el.querySelector('img'); + should.exist(icon); + (icon as HTMLImageElement).style.height.should.equal('20px');Also applies to: 143-144
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/file.test.ts` around lines 121 - 123, The test currently casts el.querySelector('img') directly to HTMLImageElement (variable icon) which can be null; update the assertions to first check existence (e.g., assert/icon.should.exist or expect(icon).to.not.be.null) before accessing icon.src and icon.style.height, and apply the same change to the similar assertions around lines 143-144 so failures show a clear "missing img" message rather than a null property access.
12-13: Prefer explicit fixture types overRecord<string, unknown>.
dataset/exportOptionsare broad and force extra casts later; a small local test type improves signal and catches misspelled fixture keys earlier.♻️ Proposed refactor
+type FileDataset = { + src?: string; + fileTitle?: string; + fileSize?: number; + fileCaption?: string; + fileName?: string; +}; + +type FileExportOptions = { + exportFormat: 'html'; + dom: typeof dom; + target?: 'email'; + postUrl?: string; +}; let editor: LexicalEditor; -let dataset: Record<string, unknown>; -let exportOptions: Record<string, unknown>; +let dataset: FileDataset; +let exportOptions: FileExportOptions;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/file.test.ts` around lines 12 - 13, The test declares dataset and exportOptions with broad types (Record<string, unknown>), which forces casts and hides typos; replace these with small explicit local fixture types (e.g., define DatasetFixture and ExportOptionsFixture interfaces/types above the tests that enumerate the exact keys used by the tests) and change the variable declarations to use those types (dataset: DatasetFixture, exportOptions: ExportOptionsFixture) so callers in file.test.ts (references to dataset and exportOptions) get proper compile-time checks and remove need for ad-hoc casts.
86-87: Extract a helper for repeatedexportDOMsetup.The same create/export/cast sequence appears multiple times. A helper would reduce duplication and keep test intent focused.
♻️ Proposed refactor
+ const exportFileElement = (inputDataset = dataset) => { + const fileNode = $createFileNode(inputDataset); + const {element} = fileNode.exportDOM(exportOptions as unknown as LexicalEditor); + return element as HTMLElement; + }; it('creates a file card', editorTest(function () { - const fileNode = $createFileNode(dataset); - const {element} = fileNode.exportDOM(exportOptions as unknown as LexicalEditor); - (element as HTMLElement).outerHTML.should.equal(`<div class="kg-card kg-file-card"><a class="kg-file-card-container" href="https://github.com/content/files/2023/03/IMG_0196.jpeg" title="Download" download=""><div class="kg-file-card-contents"><div class="kg-file-card-title">Cool image to download</div><div class="kg-file-card-caption">This is a description</div><div class="kg-file-card-metadata"><div class="kg-file-card-filename">IMG_0196.jpeg</div><div class="kg-file-card-filesize">121 KB</div></div></div><div class="kg-file-card-icon"><svg viewBox="0 0 24 24"><defs><style>.a{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.5px;}</style></defs><title>download-circle</title><polyline class="a" points="8.25 14.25 12 18 15.75 14.25"></polyline><line class="a" x1="12" y1="6.75" x2="12" y2="18"></line><circle class="a" cx="12" cy="12" r="11.25"></circle></svg></div></a></div>`); + const el = exportFileElement(); + el.outerHTML.should.equal(`<div class="kg-card kg-file-card"><a class="kg-file-card-container" href="https://github.com/content/files/2023/03/IMG_0196.jpeg" title="Download" download=""><div class="kg-file-card-contents"><div class="kg-file-card-title">Cool image to download</div><div class="kg-file-card-caption">This is a description</div><div class="kg-file-card-metadata"><div class="kg-file-card-filename">IMG_0196.jpeg</div><div class="kg-file-card-filesize">121 KB</div></div></div><div class="kg-file-card-icon"><svg viewBox="0 0 24 24"><defs><style>.a{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.5px;}</style></defs><title>download-circle</title><polyline class="a" points="8.25 14.25 12 18 15.75 14.25"></polyline><line class="a" x1="12" y1="6.75" x2="12" y2="18"></line><circle class="a" cx="12" cy="12" r="11.25"></circle></svg></div></a></div>`); }));Also applies to: 98-99, 133-134, 155-156
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/file.test.ts` around lines 86 - 87, Extract a small helper in the test (e.g., exportElement or getExportedHTMLElement) that accepts a node and exportOptions, calls node.exportDOM(exportOptions as unknown as LexicalEditor), and returns the element cast to an HTMLElement; then replace the repeated sequences that call fileNode.exportDOM(exportOptions as unknown as LexicalEditor) and (element as HTMLElement) with this helper to remove duplication and make assertions more readable (update all occurrences where exportDOM + cast is repeated).packages/kg-default-nodes/src/nodes/embed/embed-parser.ts (1)
5-5: TightenEmbedNodetyping and remove unsafe casts.The function signature uses
new (...) => unknownwhich prevents TypeScript from catching incompatible node classes. Theas LexicalNodecasts at lines 22, 53, and 72, combined with the double castas unknown as HTMLIFrameElementat line 65, further weaken type safety.Change the constructor parameter from
new (data: Record<string, unknown>) => unknowntonew (data: Record<string, unknown>) => LexicalNode, remove the unsafeas LexicalNodecasts from lines 22, 53, and 72, and replace the double cast at line 65 with a runtime type guard:♻️ Proposed refactor
import type {LexicalNode} from 'lexical'; @@ -export function parseEmbedNode(EmbedNode: new (data: Record<string, unknown>) => unknown) { +export function parseEmbedNode(EmbedNode: new (data: Record<string, unknown>) => LexicalNode) { @@ const node = new EmbedNode(payload); - return {node: node as LexicalNode}; + return {node}; @@ const node = new EmbedNode(payload); - return {node: node as LexicalNode}; + return {node}; @@ conversion(domNode: HTMLElement) { - const payload = _createPayloadForIframe(domNode as unknown as HTMLIFrameElement); + if (!(domNode instanceof HTMLIFrameElement)) { + return null; + } + const payload = _createPayloadForIframe(domNode); @@ const node = new EmbedNode(payload); - return {node: node as LexicalNode}; + return {node};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/embed/embed-parser.ts` at line 5, Change the parseEmbedNode signature to accept EmbedNode constructors that produce LexicalNode (i.e., new (data: Record<string, unknown>) => LexicalNode) instead of unknown, remove the unsafe casts to LexicalNode currently applied to instances created from EmbedNode (the casts at the spots referenced in the review), and replace the double cast to HTMLIFrameElement with a runtime type guard: after creating/receiving the element check its runtime type (e.g., element instanceof HTMLIFrameElement or element.tagName === 'IFRAME' and required properties exist) before treating it as an iframe; update usages in parseEmbedNode/EmbedNode creation to rely on the stronger constructor return type so the explicit "as LexicalNode" casts are unnecessary.packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts (1)
22-25: TightenBrowserDocument.createElementtyping with tag-specific overloads.The current signature incorrectly claims every created element has both canvas members (
getContext,width,height) and script members (innerHTML), which forces downstreamas unknown ascasts (seetransistor-renderer.ts:47). Only'canvas'and'script'tags are actually used.♻️ Suggested typing refactor
interface BrowserElement { parentElement: BrowserElement | null; querySelector(selector: string): BrowserElement | null; getAttribute(name: string): string | null; src: string; } +interface BrowserScriptElement extends BrowserElement { + innerHTML: string; +} + +interface BrowserCanvasContext { + fillStyle: string; + fillRect(x: number, y: number, w: number, h: number): void; + getImageData(x: number, y: number, w: number, h: number): { data: number[] }; +} + +interface BrowserCanvasElement extends BrowserElement { + width: number; + height: number; + getContext(id: string): BrowserCanvasContext; +} + interface BrowserDocument { currentScript: { parentElement: BrowserElement } | null; - createElement(tag: string): BrowserElement & { innerHTML: string; width: number; height: number; getContext(id: string): { fillStyle: string; fillRect(x: number, y: number, w: number, h: number): void; getImageData(x: number, y: number, w: number, h: number): { data: number[] } } }; + createElement(tag: 'script'): BrowserScriptElement; + createElement(tag: 'canvas'): BrowserCanvasElement; + createElement(tag: string): BrowserElement; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts` around lines 22 - 25, The createElement signature in BrowserDocument is too broad—every element is typed as having both canvas APIs and script innerHTML forcing unsafe casts (see transistor-renderer.ts:47); change createElement to tag-specific overloads: add distinct element types (e.g., CanvasElement with width/height/getContext/getImageData and ScriptElement with innerHTML and parentElement) and declare createElement(tag: 'canvas'): CanvasElement and createElement(tag: 'script'): ScriptElement (plus a generic createElement(tag: string): BrowserElement fallback); update BrowserDocument.currentScript typing to use ScriptElement | null and replace the single combined type with these narrower types so downstream code no longer needs as unknown as casts.packages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.ts (1)
3-3: Tighten constructor typing to eliminate unsafeLexicalNodeassertions.Typing
BookmarkNodeas returningunknownforcesas LexicalNodecasts. Change the constructor parameter to returnLexicalNodedirectly and remove the casts.Proposed fix
-export function parseBookmarkNode(BookmarkNode: new (data: Record<string, unknown>) => unknown) { +export function parseBookmarkNode(BookmarkNode: new (data: Record<string, unknown>) => LexicalNode) { ... - return {node: node as LexicalNode}; + return {node}; ... - return {node: node as LexicalNode}; + return {node};Also applies to: 31-31, 89-89
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.ts` at line 3, The constructor type for the BookmarkNode parameter in parseBookmarkNode is too loose (returns unknown) causing unsafe casts to LexicalNode; update the function signature to accept BookmarkNode: new (data: Record<string, unknown>) => LexicalNode (or the appropriate LexicalNode subclass type) and remove all `as LexicalNode` assertions inside parseBookmarkNode and the other similar factories in this file (the other constructor-typed parameters referenced in the review). Ensure imports include LexicalNode if needed and update any call sites or tests that relied on the previous signature.packages/kg-default-nodes/test/nodes/at-link.test.ts (1)
73-75: Use a real serialized payload in theimportJSON()test.
{linkFormat: 1} as ReturnType<AtLinkNode['exportJSON']>sidesteps the stricter test typing added in this PR, so this case no longer validates the actual serialized shape. Seed fromexportJSON()or pass the full object instead.Suggested cleanup
- const atLinkNode = AtLinkNode.importJSON({linkFormat: 1} as ReturnType<AtLinkNode['exportJSON']>); + const serialized: ReturnType<AtLinkNode['exportJSON']> = { + children: [], + direction: null, + format: '', + indent: 0, + linkFormat: 1, + type: 'at-link', + version: 1 + }; + const atLinkNode = AtLinkNode.importJSON(serialized);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/at-link.test.ts` around lines 73 - 75, The test currently passes a casted partial literal to AtLinkNode.importJSON which bypasses strict serialized-shape checks; instead obtain a real serialized payload by calling exportJSON() on a real AtLinkNode instance (e.g., create or obtain an AtLinkNode, call its exportJSON() to produce the serialized object) and then pass that object into AtLinkNode.importJSON(...), then assert on getLinkFormat(); replace the `{linkFormat: 1} as ReturnType<AtLinkNode['exportJSON']>` literal with the exportJSON-derived payload so the test validates the actual serialized shape.
| export function parseHorizontalRuleNode(HorizontalRuleNode: new (data?: Record<string, unknown>) => unknown) { | ||
| return { | ||
| hr: () => ({ | ||
| conversion() { | ||
| const node = new HorizontalRuleNode(); | ||
| return {node: node as LexicalNode}; | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Checking parser constructor signatures and LexicalNode casts..."
rg -nP --type=ts -C2 'new\s*\([^)]*\)\s*=>\s*unknown|as\s+LexicalNode' packages/kg-default-nodes/src/nodesRepository: TryGhost/Koenig
Length of output: 24008
Tighten constructor typing to avoid masking invalid node types.
Line 3 allows a constructor returning unknown, and Line 8 force-casts to LexicalNode. This weakens TypeScript's static guarantees and can hide incorrect constructor inputs. Type the constructor to return LexicalNode and remove the cast.
This same pattern appears in all parser files across packages/kg-default-nodes/src/nodes/. Consider a coordinated refactor across all parsers for consistency.
Proposed fix
import type {LexicalNode} from 'lexical';
-export function parseHorizontalRuleNode(HorizontalRuleNode: new (data?: Record<string, unknown>) => unknown) {
+export function parseHorizontalRuleNode(
+ HorizontalRuleNode: new (data?: Record<string, unknown>) => LexicalNode
+) {
return {
hr: () => ({
conversion() {
const node = new HorizontalRuleNode();
- return {node: node as LexicalNode};
+ return {node};
},
priority: 0 as const
})
};
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/kg-default-nodes/src/nodes/horizontalrule/horizontalrule-parser.ts`
around lines 3 - 9, The constructor parameter for parseHorizontalRuleNode is
currently typed to return unknown and the code force-casts to LexicalNode;
change the constructor signature to new (data?: Record<string, unknown>) =>
LexicalNode (i.e. require it actually constructs a LexicalNode) and remove the
cast in the conversion() implementation (use the constructed node directly);
apply the same tightening to other parser factories in
packages/kg-default-nodes/src/nodes/ so all parser constructors return
LexicalNode rather than unknown for consistent static guarantees.
| it('returns null element for exportDOM()', editorTest(function () { | ||
| const atLinkNode = $createAtLinkNode(null); | ||
| const atLinkSearchNode = $createAtLinkSearchNode('test'); | ||
| atLinkNode.append(atLinkSearchNode); | ||
| should.equal(atLinkNode.exportDOM(), null); | ||
| should.equal(atLinkNode.exportDOM().element, null); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "AtLinkNode exportDOM definition:"
sed -n '/exportDOM\s*(/,/^[[:space:]]*}/p' packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts
echo
echo "Current test call site:"
sed -n '98,103p' packages/kg-default-nodes/test/nodes/at-link.test.tsRepository: TryGhost/Koenig
Length of output: 476
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== AtLinkNode class definition and parent ==="
head -50 packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts
echo
echo "=== exportDOM method in AtLinkNode ==="
sed -n '/exportDOM/,/^[[:space:]]*}/p' packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts
echo
echo "=== Check parent class definition ==="
grep -n "extends\|class AtLinkNode" packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts | head -5
echo
echo "=== Check other node exportDOM implementations ==="
rg "exportDOM\s*\(" packages/kg-default-nodes/src/nodes/ -A 2 | head -40Repository: TryGhost/Koenig
Length of output: 3153
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== All exportDOM implementations in the codebase ==="
rg "exportDOM\s*\(" packages/kg-default-nodes/src/nodes/ -B 2 -A 4
echo
echo "=== Check node_modules for Lexical ElementNode type definitions ==="
find . -name "*.d.ts" -type f | grep -i lexical | grep -i node | head -5Repository: TryGhost/Koenig
Length of output: 1396
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Check ElementNode type definitions ==="
find . -type f \( -name "*.d.ts" -o -name "*.ts" \) | xargs grep -l "ElementNode" | grep -E "lexical|node_modules" | head -5
echo
echo "=== Check if lexical is installed and check its export signature ==="
if [ -d "node_modules/lexical" ]; then
echo "Found lexical package"
find node_modules/lexical -name "*.d.ts" -type f | head -10
echo
echo "=== Lexical ElementNode.exportDOM signature ==="
grep -r "exportDOM" node_modules/lexical/LexicalNode.d.ts 2>/dev/null | head -10
fi
echo
echo "=== Check package.json for Lexical version ==="
grep -i lexical packages/kg-default-nodes/package.jsonRepository: TryGhost/Koenig
Length of output: 435
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Lexical dependency in kg-default-nodes ==="
cat packages/kg-default-nodes/package.json | grep -A 2 -B 2 lexical
echo
echo "=== Check node_modules/lexical for type definitions ==="
ls -la node_modules/lexical/LexicalNode.d.ts 2>/dev/null || echo "Type definitions file not found, checking alternatives..."
ls -la node_modules/lexical/index.d.ts 2>/dev/null || echo "Index.d.ts not found"
echo
echo "=== Try to find exportDOM method signature in node_modules ==="
grep -r "exportDOM" node_modules/lexical/*.d.ts 2>/dev/null | head -20Repository: TryGhost/Koenig
Length of output: 873
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Search all test files for exportDOM calls ==="
rg "exportDOM\(" packages/kg-default-nodes/test/ -B 2 -A 2
echo
echo "=== Check how exportDOM is called in main code ==="
rg "exportDOM\(" packages/kg-default-nodes/src/ --not-after-path test -B 2 -A 2
echo
echo "=== Check the actual method signature by looking at the full test context ==="
sed -n '90,110p' packages/kg-default-nodes/test/nodes/at-link.test.tsRepository: TryGhost/Koenig
Length of output: 50807
Pass the test editor into exportDOM().
All other node tests in this suite pass exportOptions (cast as LexicalEditor) to exportDOM(). The test here creates a real editor through editorTest() but doesn't pass it to the method call. While AtLinkNode.exportDOM() currently returns {element: null} (and doesn't use the parameter), the method should be called according to the Lexical library's standard contract.
Suggested fix
- should.equal(atLinkNode.exportDOM().element, null);
+ should.equal(atLinkNode.exportDOM(editor).element, null);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/kg-default-nodes/test/nodes/at-link.test.ts` around lines 98 - 102,
The test calls atLinkNode.exportDOM() without the editor argument; update the
test inside editorTest to pass the test editor instance (the exportOptions /
editor provided by editorTest) into exportDOM so it follows the Lexical contract
— e.g., change the call on the $createAtLinkNode instance from exportDOM() to
exportDOM(exportOptions as unknown as LexicalEditor) (or whatever local editor
variable the editorTest callback provides) so exportDOM receives the editor
parameter.
| nodes[0].imageHeight!.should.equal(100); | ||
| })); | ||
|
|
||
| it('image width and height falls back to nulll if not provided', editorTest(function () { |
There was a problem hiding this comment.
Correct test description typo (nulll → null).
Small, but this keeps test output clean and professional.
✏️ Suggested patch
- it('image width and height falls back to nulll if not provided', editorTest(function () {
+ it('image width and height falls back to null if not provided', editorTest(function () {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it('image width and height falls back to nulll if not provided', editorTest(function () { | |
| it('image width and height falls back to null if not provided', editorTest(function () { |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/kg-default-nodes/test/nodes/call-to-action.test.ts` at line 805,
Update the test description string in the test case currently named "image width
and height falls back to nulll if not provided" to correct the typo by replacing
"nulll" with "null" (the test wrapped with editorTest(...) / the it(...) call
that asserts image width/height fallback). Keep the rest of the test logic
unchanged.
| import {DEFAULT_CONFIG, DEFAULT_NODES} from '../../src/index.js'; | ||
| import type {HTMLConfig, LexicalEditor} from 'lexical'; | ||
|
|
||
| describe('Serializers: linebreak', function () { |
There was a problem hiding this comment.
Fix test suite title to match file intent.
Line 8 labels this suite as linebreak, but this file tests paragraph behavior. This makes test reporting confusing.
🛠️ Suggested fix
-describe('Serializers: linebreak', function () {
+describe('Serializers: paragraph', function () {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| describe('Serializers: linebreak', function () { | |
| describe('Serializers: paragraph', function () { |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/kg-default-nodes/test/serializers/paragraph.test.ts` at line 8,
Update the test suite title in the describe call used in paragraph.test.ts: the
current suite name string is "Serializers: linebreak" but the file and tests
target paragraph behavior, so change that describe(...) title to "Serializers:
paragraph" (locate the describe invocation in paragraph.test.ts and edit the
first argument string accordingly).
no issue This groups the verified parser, renderer, factory, and type-guard fixes so the node APIs stay aligned with their runtime behavior and edge-case tests.
no issue This preserves property-defined dataset shapes in generateDecoratorNode so generated node factories and constructors stop erasing their input types back to Record<string, unknown>.
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (1)
packages/kg-default-nodes/src/nodes/html/html-parser.ts (1)
24-40:⚠️ Potential issue | 🟠 MajorReturn
nullwhen the closing HTML marker is missing.If
kg-card-end: htmlis not found, this still creates an emptyHtmlNode, which turns malformed marker input into a synthetic empty card. The safer behavior is to bail out early and not convert.Suggested fix
- if (hasEndComment) { - while (nextNode && !isHtmlEndComment(nextNode)) { - const currentNode = nextNode; - nextNode = currentNode.nextSibling; - if (currentNode.nodeType === 1) { - html.push((currentNode as Element).outerHTML); - } else if (currentNode.nodeType === 3 && currentNode.textContent) { - html.push(currentNode.textContent); - } - // remove nodes as we go so that they don't go through the parser - currentNode.remove(); - } - } + if (!hasEndComment) { + return null; + } + + while (nextNode && !isHtmlEndComment(nextNode)) { + const currentNode = nextNode; + nextNode = currentNode.nextSibling; + if (currentNode.nodeType === 1) { + html.push((currentNode as Element).outerHTML); + } else if (currentNode.nodeType === 3 && currentNode.textContent) { + html.push(currentNode.textContent); + } + // remove nodes as we go so that they don't go through the parser + currentNode.remove(); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/html/html-parser.ts` around lines 24 - 40, The code currently creates an empty HtmlNode even when the closing marker is missing; change the logic in html-parser.ts to bail out when hasEndComment is false by returning null (or the parser's expected "no node" value) instead of constructing a payload/HtmlNode. Concretely, use the existing hasEndComment flag (and the isHtmlEndComment logic) to early-return null before building the html array/payload and instantiating HtmlNode so malformed/missing closing `kg-card-end: html` markers are not turned into synthetic empty cards.
🧹 Nitpick comments (8)
packages/kg-default-nodes/src/nodes/paywall/PaywallNode.ts (1)
19-21: Make$createPaywallNodedataset optional for this empty-schema node.Since this node has no declared properties, requiring a dataset argument adds avoidable call-site noise.
Suggested tweak
-export const $createPaywallNode = (dataset: PaywallData) => { - return new PaywallNode(dataset); +export const $createPaywallNode = (dataset: PaywallData = {}) => { + return new PaywallNode(dataset); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/paywall/PaywallNode.ts` around lines 19 - 21, Make the dataset parameter optional on the factory: change $createPaywallNode to accept dataset?: PaywallData and call new PaywallNode(dataset ?? {}) (or otherwise allow undefined) so callers can omit the argument for this empty-schema node; update the function signature and the single instantiation (new PaywallNode(...)) accordingly, referencing $createPaywallNode and PaywallNode.packages/kg-default-nodes/src/nodes/image/ImageNode.ts (1)
26-43: ReduceexportJSON()drift risk by deriving fromsuper.exportJSON().
exportJSON()manually mirrors all properties, so futureimagePropertieschanges can silently desync serialization. Prefer extending the base export and only redactingsrc.♻️ Proposed refactor
exportJSON() { - // checks if src is a data string - const {src, width, height, title, alt, caption, cardWidth, href} = this; - const isBlob = src && src.startsWith('data:'); - - const dataset = { - type: 'image', - version: 1, - src: isBlob ? '<base64String>' : src, - width, - height, - title, - alt, - caption, - cardWidth, - href - }; - return dataset; + const dataset = super.exportJSON() as Record<string, unknown> & {src?: string}; + if (typeof dataset.src === 'string' && dataset.src.startsWith('data:')) { + dataset.src = '<base64String>'; + } + return dataset; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/image/ImageNode.ts` around lines 26 - 43, The exportJSON method duplicates all image fields instead of deriving them from the base class, risking drift; modify ImageNode.exportJSON to call super.exportJSON() to get the canonical dataset (so it inherits any future imageProperties changes), then detect if this.src startsWith('data:') and replace dataset.src with '<base64String>' when needed, otherwise leave dataset.src as-is, and finally return that dataset (preserving all other properties from super.exportJSON()).packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts (1)
26-27: Consider trimming values inisEmpty()to avoid whitespace-only false negatives.At Lines 26-27, whitespace-only
__url/__htmlwill mark the node as non-empty.Suggested tweak
isEmpty() { - return !this.__url && !this.__html; + return !this.__url.trim() && !this.__html.trim(); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts` around lines 26 - 27, The isEmpty() method currently checks __url and __html raw values which treats whitespace-only strings as non-empty; update EmbedNode.isEmpty() to trim both this.__url and this.__html (e.g., use String.prototype.trim on each) before evaluating emptiness so whitespace-only values are treated as empty, and ensure you reference the __url and __html properties in the check.packages/kg-default-nodes/src/nodes/header/HeaderNode.ts (1)
16-41: Consider replacing version magic numbers with shared constants.Using shared constants for render version keys would reduce drift risk between
properties.versionanddefaultRenderFn.♻️ Proposed refactor
+const HEADER_VERSION = { + V1: 1, + V2: 2 +} as const; + const headerProperties = [ @@ - {name: 'version', default: 1}, + {name: 'version', default: HEADER_VERSION.V1}, @@ export class HeaderNode extends generateDecoratorNode({ @@ defaultRenderFn: { - 1: renderHeaderNodeV1, - 2: renderHeaderNodeV2 + [HEADER_VERSION.V1]: renderHeaderNodeV1, + [HEADER_VERSION.V2]: renderHeaderNodeV2 } }) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/header/HeaderNode.ts` around lines 16 - 41, The header node currently uses literal version numbers in headerProperties (property 'version' default: 1) and in the HeaderNode class defaultRenderFn mapping (keys 1 and 2), which can drift; introduce shared constants (e.g., HEADER_RENDER_V1, HEADER_RENDER_V2 or a HeaderRenderVersion enum) and replace the numeric literals used in headerProperties and the defaultRenderFn keys, then update the mapping to reference those constants alongside renderHeaderNodeV1 and renderHeaderNodeV2 so the version key is defined in one place and reused.packages/kg-default-nodes/test/nodes/gallery.test.ts (2)
1061-1061: Prefer explicit existence assertions over non-null assertions on regex matches.
!can throw before giving a clear assertion message when the match is missing.✅ Suggested assertion style
- sizes!.length.should.equal(2); + should.exist(sizes); + sizes.length.should.equal(2); @@ - output.match(/width="600"/g)!.length.should.equal(3); + const widthMatches = output.match(/width="600"/g); + should.exist(widthMatches); + widthMatches.length.should.equal(3);Also applies to: 1170-1170
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/gallery.test.ts` at line 1061, The test currently uses a non-null assertion when reading a regex match result (e.g., the sizes variable from a .match call) which can throw before a clear failure message; change it to first assert the match exists (e.g., assert.exists/sizes.should.exist or expect(sizes).to.not.be.null/undefined) and only then assert on its length (e.g., have.lengthOf/length.should.equal(2)), updating both the use of sizes and the other similar match assertion so failures show a clear assertion message instead of a runtime error.
638-639: Extract repeatedouterHTMLassertion plumbing into a helper.The repeated cast-and-read pattern adds noise and makes future assertion updates harder.
♻️ Suggested cleanup
describe('GalleryNode', function () { + const getOuterHTML = (element: Node): string => (element as HTMLElement).outerHTML; + let editor: LexicalEditor; let dataset: Record<string, unknown>; let exportOptions: Record<string, unknown>; @@ - (element as HTMLElement).outerHTML.should.equal('<span></span>'); + getOuterHTML(element).should.equal('<span></span>'); @@ - const output = (element as HTMLElement).outerHTML; + const output = getOuterHTML(element);Also applies to: 645-646, 652-653, 693-694, 720-721, 759-760, 794-795, 883-884, 933-934, 967-968, 994-995, 1011-1012, 1028-1029, 1058-1059, 1080-1081, 1096-1097, 1125-1126, 1167-1168, 1202-1203
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/test/nodes/gallery.test.ts` around lines 638 - 639, Extract the repeated cast-and-outerHTML assertion into a small helper (e.g., assertOuterHTML or expectOuterHTML) inside the test file and replace every occurrence of "(element as HTMLElement).outerHTML.should.equal(...)" with a call to that helper; the helper should accept the Element and expected string and perform the cast and .outerHTML.should.equal check so you only change the assertion calls in tests (update all listed occurrences and any similar lines such as at 645-646, 652-653, 693-694, 720-721, 759-760, 794-795, 883-884, 933-934, 967-968, 994-995, 1011-1012, 1028-1029, 1058-1059, 1080-1081, 1096-1097, 1125-1126, 1167-1168, 1202-1203).packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts (2)
116-118: Make$createBookmarkNodeparameter optional for parity with constructor defaults.Line 116 currently requires
dataset, butconstructoralready supports empty input. Defaulting this to{}avoids extra caller boilerplate.✨ Suggested small API polish
-export const $createBookmarkNode = (dataset: BookmarkData) => { +export const $createBookmarkNode = (dataset: BookmarkData = {}) => { return new BookmarkNode(dataset); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts` around lines 116 - 118, The factory function $createBookmarkNode currently requires a BookmarkData parameter even though BookmarkNode's constructor accepts empty input; change $createBookmarkNode to make its dataset parameter optional and default it to {} so callers can invoke $createBookmarkNode() without boilerplate, and ensure the function still forwards the (possibly empty) object into new BookmarkNode(dataset) to preserve behavior.
82-89: Avoid blind casting inimportJSON.Line 83 trusts unknown JSON with
as BookmarkData. A malformed payload can still populate non-string runtime values. Normalize/guard fields before constructing the node.♻️ Suggested runtime-safe import normalization
static importJSON(serializedNode: Record<string, unknown>) { - const {url, metadata, caption} = serializedNode as BookmarkData; - const node = new this({ - url, - metadata, - caption - }); - return node; + const asString = (value: unknown) => typeof value === 'string' ? value : ''; + const metadata = (typeof serializedNode.metadata === 'object' && serializedNode.metadata !== null) + ? serializedNode.metadata as Record<string, unknown> + : {}; + + return new this({ + url: asString(serializedNode.url), + metadata: { + icon: asString(metadata.icon), + title: asString(metadata.title), + description: asString(metadata.description), + author: asString(metadata.author), + publisher: asString(metadata.publisher), + thumbnail: asString(metadata.thumbnail) + }, + caption: asString(serializedNode.caption) + }); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts` around lines 82 - 89, The importJSON method currently casts the incoming serializedNode to BookmarkData blindly; change importJSON to safely extract and normalize fields (url, metadata, caption) from serializedNode by checking types and providing defaults — e.g., ensure url and caption are strings (or empty string/default), ensure metadata is an object (or null/{}), and coerce or ignore invalid values — then pass these sanitized values into the constructor call (the new this({...}) invocation) instead of using the unchecked as BookmarkData cast.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts`:
- Around line 29-33: The constructor in CalloutNode uses a strict !== undefined
check for calloutEmoji which lets null slip through; update the assignment in
the constructor (the CalloutNode constructor that sets this.__calloutEmoji) to
use nullish coalescing (?? '💡') so null and undefined both fall back to the
default emoji while preserving explicit empty-string values.
In `@packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts`:
- Line 9: The metadata property currently uses a shared mutable default ({})
causing all EmbedNode instances to share the same object; update the metadata
property descriptor in EmbedNode to return a fresh object per instance (e.g.,
use get default() { return {}; } or a factory) so that the constructor code in
generate-decorator-node.ts (which sets this.__metadata = dataset['metadata'] ||
prop.default) gets a new object for each node instead of a shared
reference—mirror the pattern used by visibility (get default() { return
buildDefaultVisibility(); }) to fix the leak.
In `@packages/kg-default-nodes/src/nodes/file/FileNode.ts`:
- Around line 26-31: The sanitization only treats data: URIs as transient;
update the logic that computes isBlob (in FileNode.ts, where isBlob is derived
from src) to also detect blob: URLs (e.g., src.startsWith('data:') ||
src.startsWith('blob:')) so the returned object for the file node (type: 'file',
version: 1, src: ...) masks both data and blob URLs (still using the placeholder
'<base64String>' or your chosen placeholder) before export.
In `@packages/kg-default-nodes/src/nodes/signup/SignupNode.ts`:
- Around line 65-77: The constructor and setLabels are storing the incoming
labels array by reference (assigning to this.__labels), which allows external
mutation to affect node state; update both the SignupNode constructor and the
setLabels method to clone the incoming labels array (e.g., use a shallow copy
like [...labels] or labels.slice()) before assigning to this.__labels so the
node keeps its own copy and is immune to external mutations.
---
Duplicate comments:
In `@packages/kg-default-nodes/src/nodes/html/html-parser.ts`:
- Around line 24-40: The code currently creates an empty HtmlNode even when the
closing marker is missing; change the logic in html-parser.ts to bail out when
hasEndComment is false by returning null (or the parser's expected "no node"
value) instead of constructing a payload/HtmlNode. Concretely, use the existing
hasEndComment flag (and the isHtmlEndComment logic) to early-return null before
building the html array/payload and instantiating HtmlNode so malformed/missing
closing `kg-card-end: html` markers are not turned into synthetic empty cards.
---
Nitpick comments:
In `@packages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.ts`:
- Around line 116-118: The factory function $createBookmarkNode currently
requires a BookmarkData parameter even though BookmarkNode's constructor accepts
empty input; change $createBookmarkNode to make its dataset parameter optional
and default it to {} so callers can invoke $createBookmarkNode() without
boilerplate, and ensure the function still forwards the (possibly empty) object
into new BookmarkNode(dataset) to preserve behavior.
- Around line 82-89: The importJSON method currently casts the incoming
serializedNode to BookmarkData blindly; change importJSON to safely extract and
normalize fields (url, metadata, caption) from serializedNode by checking types
and providing defaults — e.g., ensure url and caption are strings (or empty
string/default), ensure metadata is an object (or null/{}), and coerce or ignore
invalid values — then pass these sanitized values into the constructor call (the
new this({...}) invocation) instead of using the unchecked as BookmarkData cast.
In `@packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts`:
- Around line 26-27: The isEmpty() method currently checks __url and __html raw
values which treats whitespace-only strings as non-empty; update
EmbedNode.isEmpty() to trim both this.__url and this.__html (e.g., use
String.prototype.trim on each) before evaluating emptiness so whitespace-only
values are treated as empty, and ensure you reference the __url and __html
properties in the check.
In `@packages/kg-default-nodes/src/nodes/header/HeaderNode.ts`:
- Around line 16-41: The header node currently uses literal version numbers in
headerProperties (property 'version' default: 1) and in the HeaderNode class
defaultRenderFn mapping (keys 1 and 2), which can drift; introduce shared
constants (e.g., HEADER_RENDER_V1, HEADER_RENDER_V2 or a HeaderRenderVersion
enum) and replace the numeric literals used in headerProperties and the
defaultRenderFn keys, then update the mapping to reference those constants
alongside renderHeaderNodeV1 and renderHeaderNodeV2 so the version key is
defined in one place and reused.
In `@packages/kg-default-nodes/src/nodes/image/ImageNode.ts`:
- Around line 26-43: The exportJSON method duplicates all image fields instead
of deriving them from the base class, risking drift; modify ImageNode.exportJSON
to call super.exportJSON() to get the canonical dataset (so it inherits any
future imageProperties changes), then detect if this.src startsWith('data:') and
replace dataset.src with '<base64String>' when needed, otherwise leave
dataset.src as-is, and finally return that dataset (preserving all other
properties from super.exportJSON()).
In `@packages/kg-default-nodes/src/nodes/paywall/PaywallNode.ts`:
- Around line 19-21: Make the dataset parameter optional on the factory: change
$createPaywallNode to accept dataset?: PaywallData and call new
PaywallNode(dataset ?? {}) (or otherwise allow undefined) so callers can omit
the argument for this empty-schema node; update the function signature and the
single instantiation (new PaywallNode(...)) accordingly, referencing
$createPaywallNode and PaywallNode.
In `@packages/kg-default-nodes/test/nodes/gallery.test.ts`:
- Line 1061: The test currently uses a non-null assertion when reading a regex
match result (e.g., the sizes variable from a .match call) which can throw
before a clear failure message; change it to first assert the match exists
(e.g., assert.exists/sizes.should.exist or
expect(sizes).to.not.be.null/undefined) and only then assert on its length
(e.g., have.lengthOf/length.should.equal(2)), updating both the use of sizes and
the other similar match assertion so failures show a clear assertion message
instead of a runtime error.
- Around line 638-639: Extract the repeated cast-and-outerHTML assertion into a
small helper (e.g., assertOuterHTML or expectOuterHTML) inside the test file and
replace every occurrence of "(element as
HTMLElement).outerHTML.should.equal(...)" with a call to that helper; the helper
should accept the Element and expected string and perform the cast and
.outerHTML.should.equal check so you only change the assertion calls in tests
(update all listed occurrences and any similar lines such as at 645-646,
652-653, 693-694, 720-721, 759-760, 794-795, 883-884, 933-934, 967-968, 994-995,
1011-1012, 1028-1029, 1058-1059, 1080-1081, 1096-1097, 1125-1126, 1167-1168,
1202-1203).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3cf6a4b6-5aaf-43ad-8abd-c7a05c01336b
📒 Files selected for processing (31)
packages/kg-default-nodes/src/generate-decorator-node.tspackages/kg-default-nodes/src/nodes/audio/AudioNode.tspackages/kg-default-nodes/src/nodes/bookmark/BookmarkNode.tspackages/kg-default-nodes/src/nodes/button/ButtonNode.tspackages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.tspackages/kg-default-nodes/src/nodes/callout/CalloutNode.tspackages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.tspackages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.tspackages/kg-default-nodes/src/nodes/email/EmailNode.tspackages/kg-default-nodes/src/nodes/embed/EmbedNode.tspackages/kg-default-nodes/src/nodes/file/FileNode.tspackages/kg-default-nodes/src/nodes/gallery/GalleryNode.tspackages/kg-default-nodes/src/nodes/gallery/gallery-parser.tspackages/kg-default-nodes/src/nodes/header/HeaderNode.tspackages/kg-default-nodes/src/nodes/html/HtmlNode.tspackages/kg-default-nodes/src/nodes/html/html-parser.tspackages/kg-default-nodes/src/nodes/image/ImageNode.tspackages/kg-default-nodes/src/nodes/markdown/MarkdownNode.tspackages/kg-default-nodes/src/nodes/paywall/PaywallNode.tspackages/kg-default-nodes/src/nodes/product/ProductNode.tspackages/kg-default-nodes/src/nodes/signup/SignupNode.tspackages/kg-default-nodes/src/nodes/toggle/ToggleNode.tspackages/kg-default-nodes/src/nodes/transistor/TransistorNode.tspackages/kg-default-nodes/src/nodes/video/VideoNode.tspackages/kg-default-nodes/src/nodes/video/video-renderer.tspackages/kg-default-nodes/test/nodes/bookmark.test.tspackages/kg-default-nodes/test/nodes/gallery.test.tspackages/kg-default-nodes/test/nodes/html.test.tspackages/kg-default-nodes/test/nodes/signup.test.tspackages/kg-default-nodes/test/nodes/transistor.test.tspackages/kg-default-nodes/test/nodes/video.test.ts
✅ Files skipped from review due to trivial changes (1)
- packages/kg-default-nodes/test/nodes/bookmark.test.ts
🚧 Files skipped from review as they are similar to previous changes (13)
- packages/kg-default-nodes/test/nodes/signup.test.ts
- packages/kg-default-nodes/src/nodes/product/ProductNode.ts
- packages/kg-default-nodes/src/nodes/audio/AudioNode.ts
- packages/kg-default-nodes/src/nodes/gallery/gallery-parser.ts
- packages/kg-default-nodes/src/nodes/codeblock/CodeBlockNode.ts
- packages/kg-default-nodes/test/nodes/html.test.ts
- packages/kg-default-nodes/src/nodes/gallery/GalleryNode.ts
- packages/kg-default-nodes/test/nodes/video.test.ts
- packages/kg-default-nodes/src/nodes/call-to-action/CallToActionNode.ts
- packages/kg-default-nodes/test/nodes/transistor.test.ts
- packages/kg-default-nodes/src/generate-decorator-node.ts
- packages/kg-default-nodes/src/nodes/email-cta/EmailCtaNode.ts
- packages/kg-default-nodes/src/nodes/video/video-renderer.ts
| constructor({calloutText, calloutEmoji, backgroundColor}: CalloutData = {}, key?: string) { | ||
| super({}, key); | ||
| this.__calloutText = calloutText || ''; | ||
| this.__calloutEmoji = calloutEmoji !== undefined ? calloutEmoji : '💡'; | ||
| this.__backgroundColor = backgroundColor || 'blue'; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether null can reach calloutEmoji from parser/fixtures/serialization paths
rg -nP --type=ts -C3 'calloutEmoji\s*:\s*null|calloutEmoji\s*==\s*null|calloutEmoji\s*===\s*null'
rg -nP --type=ts -C3 'getAttribute\([^)]*callout-emoji|dataset\.[A-Za-z_]*calloutEmoji|calloutEmoji'Repository: TryGhost/Koenig
Length of output: 20893
Use nullish coalescing for calloutEmoji default.
The current !== undefined check allows null to pass through at runtime. While the TypeScript interface and all parsers ensure calloutEmoji is always a string (never null), using nullish coalescing is safer and more idiomatic:
Proposed fix
- this.__calloutEmoji = calloutEmoji !== undefined ? calloutEmoji : '💡';
+ this.__calloutEmoji = calloutEmoji ?? '💡';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/kg-default-nodes/src/nodes/callout/CalloutNode.ts` around lines 29 -
33, The constructor in CalloutNode uses a strict !== undefined check for
calloutEmoji which lets null slip through; update the assignment in the
constructor (the CalloutNode constructor that sets this.__calloutEmoji) to use
nullish coalescing (?? '💡') so null and undefined both fall back to the default
emoji while preserving explicit empty-string values.
| {name: 'url', default: '', urlType: 'url'}, | ||
| {name: 'embedType', default: ''}, | ||
| {name: 'html', default: ''}, | ||
| {name: 'metadata', default: {} as Record<string, unknown>}, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
TARGET="packages/kg-default-nodes/src/generate-decorator-node.ts"
echo "Inspecting constructor/default handling in $TARGET"
nl -ba "$TARGET" | sed -n '1,260p'
echo
echo "Searching for property default assignment patterns"
rg -n --type=ts -C3 '\bconstructor\b|\bdefault\b|internalProps|dataset|privateName|__' "$TARGET"
echo
echo "Embed metadata default declaration"
nl -ba packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts | sed -n '5,15p'Repository: TryGhost/Koenig
Length of output: 218
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Inspecting generate-decorator-node.ts (first 300 lines) ==="
if [ -f "packages/kg-default-nodes/src/generate-decorator-node.ts" ]; then
cat -n "packages/kg-default-nodes/src/generate-decorator-node.ts" | head -300
else
echo "File not found"
fi
echo ""
echo "=== Searching for default assignment and cloning patterns ==="
rg -n --type=ts 'default|clone|spread|\.\.\.|assign|Object\.assign' packages/kg-default-nodes/src/generate-decorator-node.ts -A 2 -B 1
echo ""
echo "=== Full EmbedNode.ts for context ==="
if [ -f "packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts" ]; then
cat -n "packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts"
else
echo "File not found"
fiRepository: TryGhost/Koenig
Length of output: 21180
Mutable default object in metadata is shared across node instances.
Line 9 uses a mutable object literal as default. The constructor (lines 152–156 of generate-decorator-node.ts) assigns defaults by reference without cloning: this.__metadata = dataset['metadata'] || prop.default. When a new EmbedNode is created without metadata data, all instances will reference the same {} object, causing mutations in one node to leak to others.
The codebase already demonstrates the correct pattern: the visibility property (lines 134–142) uses a getter (get default() { return buildDefaultVisibility(); }) to avoid this issue. Apply the same pattern to metadata:
{name: 'metadata', get default() { return {}; }, urlType?: undefined}Or provide a factory function to create fresh objects per instance.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/kg-default-nodes/src/nodes/embed/EmbedNode.ts` at line 9, The
metadata property currently uses a shared mutable default ({}) causing all
EmbedNode instances to share the same object; update the metadata property
descriptor in EmbedNode to return a fresh object per instance (e.g., use get
default() { return {}; } or a factory) so that the constructor code in
generate-decorator-node.ts (which sets this.__metadata = dataset['metadata'] ||
prop.default) gets a new object for each node instead of a shared
reference—mirror the pattern used by visibility (get default() { return
buildDefaultVisibility(); }) to fix the leak.
| const isBlob = src && src.startsWith('data:'); | ||
|
|
||
| return { | ||
| type: 'file' as const, | ||
| version: 1, | ||
| src: isBlob ? '<base64String>' : src, |
There was a problem hiding this comment.
Handle blob: URLs in src sanitization too.
Line 26 only checks data: URIs. blob: URLs are also transient and should be masked before JSON export to avoid persisting non-portable values.
Suggested patch
- const isBlob = src && src.startsWith('data:');
+ const isBlob = src.startsWith('data:') || src.startsWith('blob:');📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const isBlob = src && src.startsWith('data:'); | |
| return { | |
| type: 'file' as const, | |
| version: 1, | |
| src: isBlob ? '<base64String>' : src, | |
| const isBlob = src && (src.startsWith('data:') || src.startsWith('blob:')); | |
| return { | |
| type: 'file' as const, | |
| version: 1, | |
| src: isBlob ? '<base64String>' : src, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/kg-default-nodes/src/nodes/file/FileNode.ts` around lines 26 - 31,
The sanitization only treats data: URIs as transient; update the logic that
computes isBlob (in FileNode.ts, where isBlob is derived from src) to also
detect blob: URLs (e.g., src.startsWith('data:') || src.startsWith('blob:')) so
the returned object for the file node (type: 'file', version: 1, src: ...) masks
both data and blob URLs (still using the placeholder '<base64String>' or your
chosen placeholder) before export.
| constructor({alignment, backgroundColor, backgroundImageSrc, backgroundSize, textColor, buttonColor, buttonTextColor, buttonText, disclaimer, header, labels, layout, subheader, successMessage, swapped} : SignupData = {}, key?: string) { | ||
| super({}, key); | ||
| this.__alignment = alignment || 'left'; | ||
| this.__backgroundColor = backgroundColor || '#F0F0F0'; | ||
| this.__backgroundImageSrc = backgroundImageSrc || ''; | ||
| this.__backgroundSize = backgroundSize || 'cover'; | ||
| this.__textColor = (backgroundColor === 'transparent' && (layout === 'split' || !backgroundImageSrc)) ? '' : textColor || '#000000'; // text color should inherit with a transparent bg color unless we're using an image for the background (which supercedes the bg color) | ||
| this.__buttonColor = buttonColor || 'accent'; | ||
| this.__buttonTextColor = buttonTextColor || '#FFFFFF'; | ||
| this.__buttonText = buttonText || 'Subscribe'; | ||
| this.__disclaimer = disclaimer || ''; | ||
| this.__header = header || ''; | ||
| this.__labels = labels || []; |
There was a problem hiding this comment.
Clone incoming labels arrays to prevent external mutation leaks.
labels is stored by reference in both the constructor and setLabels. If the caller mutates the original array later, node state can change unintentionally outside expected update flow.
Suggested fix
constructor({alignment, backgroundColor, backgroundImageSrc, backgroundSize, textColor, buttonColor, buttonTextColor, buttonText, disclaimer, header, labels, layout, subheader, successMessage, swapped} : SignupData = {}, key?: string) {
super({}, key);
@@
- this.__labels = labels || [];
+ this.__labels = Array.isArray(labels) ? [...labels] : [];
@@
setLabels(labels: string[]) {
@@
const writable = this.getWritable();
- writable.__labels = labels;
+ writable.__labels = [...labels];
}Also applies to: 90-97
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/kg-default-nodes/src/nodes/signup/SignupNode.ts` around lines 65 -
77, The constructor and setLabels are storing the incoming labels array by
reference (assigning to this.__labels), which allows external mutation to affect
node state; update both the SignupNode constructor and the setLabels method to
clone the incoming labels array (e.g., use a shallow copy like [...labels] or
labels.slice()) before assigning to this.__labels so the node keeps its own copy
and is immune to external mutations.
no issue This groups the verified fixes for serializer contracts and defensive parser and renderer handling so malformed or partial node data no longer produces broken output or DOM errors.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts (1)
58-61:⚠️ Potential issue | 🟡 MinorRestore default placeholder when text becomes empty.
Line 59 always resolves to
this.__placeholder ?? '', so after text is cleared with no custom placeholder, the default placeholder is never restored.updateDOMshould mirrorcreateDOM’s empty-text behavior.💡 Suggested fix
updateDOM(prevNode: AtLinkSearchNode, dom: HTMLElement, config: EditorConfig) { - dom.dataset.placeholder = this.__placeholder ?? ''; + dom.dataset.placeholder = this.__text + ? (this.__placeholder ?? '') + : (this.__placeholder ?? this.defaultPlaceholder); return super.updateDOM(prevNode, dom, config); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts` around lines 58 - 61, updateDOM currently always sets dom.dataset.placeholder = this.__placeholder ?? '' so the default placeholder is never restored when text is cleared; change updateDOM to mirror createDOM's empty-text behavior by computing the placeholder based on whether the node's text is empty, e.g. if the node is empty use this.__placeholder if present otherwise use the same default placeholder used in createDOM (refer to AtLinkSearchNode.createDOM and the default placeholder constant or getter), and if not empty clear the placeholder; update the dom.dataset.placeholder assignment in updateDOM accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@packages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.ts`:
- Around line 58-61: updateDOM currently always sets dom.dataset.placeholder =
this.__placeholder ?? '' so the default placeholder is never restored when text
is cleared; change updateDOM to mirror createDOM's empty-text behavior by
computing the placeholder based on whether the node's text is empty, e.g. if the
node is empty use this.__placeholder if present otherwise use the same default
placeholder used in createDOM (refer to AtLinkSearchNode.createDOM and the
default placeholder constant or getter), and if not empty clear the placeholder;
update the dom.dataset.placeholder assignment in updateDOM accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 1741f484-8b16-4db0-9335-fcaf14e5426d
📒 Files selected for processing (11)
packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.tspackages/kg-default-nodes/src/nodes/at-link/AtLinkSearchNode.tspackages/kg-default-nodes/src/nodes/audio/audio-renderer.tspackages/kg-default-nodes/src/nodes/bookmark/bookmark-parser.tspackages/kg-default-nodes/src/nodes/embed/types/twitter.tspackages/kg-default-nodes/src/utils/set-src-background-from-parent.tspackages/kg-default-nodes/test/nodes/at-link-search.test.tspackages/kg-default-nodes/test/nodes/at-link.test.tspackages/kg-default-nodes/test/nodes/audio.test.tspackages/kg-default-nodes/test/nodes/bookmark.test.tspackages/kg-default-nodes/test/nodes/embed.test.ts
🚧 Files skipped from review as they are similar to previous changes (6)
- packages/kg-default-nodes/src/utils/set-src-background-from-parent.ts
- packages/kg-default-nodes/test/nodes/embed.test.ts
- packages/kg-default-nodes/src/nodes/at-link/AtLinkNode.ts
- packages/kg-default-nodes/test/nodes/bookmark.test.ts
- packages/kg-default-nodes/test/nodes/audio.test.ts
- packages/kg-default-nodes/src/nodes/embed/types/twitter.ts
Summary
anytypes andeslint-disablecomments from source (Visibility interface, set-src-background-from-parent browser types, generate-decorator-node exportJSON)tsconfig.test.jsonwith full test type-checking — all 30+ test files converted frombuild/cjstosrcimports with proper type annotations"node"condition),moduleResolution→NodeNext,tseslint.config()→defineConfig()overrides.tstyping withas unknown aspattern