Skip to content

BodyText linting part 2: no-invalid-bodytext-children#3015

Open
marcysutton wants to merge 10 commits intomainfrom
WB-2248-bodytext-children
Open

BodyText linting part 2: no-invalid-bodytext-children#3015
marcysutton wants to merge 10 commits intomainfrom
WB-2248-bodytext-children

Conversation

@marcysutton
Copy link
Copy Markdown
Member

@marcysutton marcysutton commented Apr 16, 2026

Summary:

This new lint rule will flag invalid children within BodyText:

  • viewChild — always warns when View is a direct child of BodyText, regardless of tag prop
  • divChild — warns when <div> is a direct child and BodyText renders as <p> (no tag or tag="p")
  • paragraphChild — warns when <p> or a default <BodyText> (renders as <p>) is a direct child
  • blockChild — warns on other block-level HTML elements (section, ul, h1h6, etc.) and WB heading components when BodyText renders as <p>
  • tooManyChildren — warns when direct JSX element children exceed a configurable threshold (default: 5)
  • Block-child checks are skipped when BodyText has an inline or block-container tag prop set

Issue: WB-2248

Test plan:

  1. Ensure tests pass
  2. Test in frontend (WIP)

@marcysutton marcysutton self-assigned this Apr 16, 2026
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 16, 2026

🦋 Changeset detected

Latest commit: 2a2725d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
eslint-plugin-wonder-blocks-demo Minor
@khanacademy/eslint-plugin-wonder-blocks Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@khan-actions-bot khan-actions-bot requested a review from a team April 16, 2026 23:12
@khan-actions-bot
Copy link
Copy Markdown
Contributor

khan-actions-bot commented Apr 16, 2026

Gerald

Required Reviewers
  • @Khan/wonder-blocks for changes to .eslintrc.js, pnpm-lock.yaml, .changeset/shy-ducks-collect.md, packages/eslint-plugin-wonder-blocks/README.md, packages/eslint-plugin-wonder-blocks/docs/no-invalid-bodytext-children.md, packages/eslint-plugin-wonder-blocks/demo/src/no-invalid-bodytext-children-example.tsx, packages/eslint-plugin-wonder-blocks/src/configs/strict.ts, packages/eslint-plugin-wonder-blocks/src/rules/index.ts, packages/eslint-plugin-wonder-blocks/src/rules/jsx-utils.ts, packages/eslint-plugin-wonder-blocks/src/rules/no-invalid-bodytext-children.ts, packages/eslint-plugin-wonder-blocks/src/rules/__tests__/jsx-utils.test.ts, packages/eslint-plugin-wonder-blocks/src/rules/__tests__/no-invalid-bodytext-children.test.ts

Don't want to be involved in this pull request? Comment #removeme and we won't notify you of further changes.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 16, 2026

Size Change: +819 B (+0.66%)

Total Size: 124 kB

📦 View Changed
Filename Size Change
packages/eslint-plugin-wonder-blocks/dist/es/index.js 2.81 kB +819 B (+41.03%) 🚨
ℹ️ View Unchanged
Filename Size
packages/wonder-blocks-accordion/dist/es/index.js 3.01 kB
packages/wonder-blocks-announcer/dist/es/index.js 2.43 kB
packages/wonder-blocks-badge/dist/es/index.js 2.02 kB
packages/wonder-blocks-banner/dist/es/index.js 2.01 kB
packages/wonder-blocks-birthday-picker/dist/es/index.js 1.93 kB
packages/wonder-blocks-breadcrumbs/dist/es/index.js 755 B
packages/wonder-blocks-button/dist/es/index.js 4.28 kB
packages/wonder-blocks-card/dist/es/index.js 1.08 kB
packages/wonder-blocks-cell/dist/es/index.js 2.18 kB
packages/wonder-blocks-clickable/dist/es/index.js 2.6 kB
packages/wonder-blocks-core/dist/es/index.js 2.59 kB
packages/wonder-blocks-data/dist/es/index.js 5.48 kB
packages/wonder-blocks-date-picker/dist/es/index.js 8.06 kB
packages/wonder-blocks-dropdown/dist/es/index.js 19.7 kB
packages/wonder-blocks-form/dist/es/index.js 6.29 kB
packages/wonder-blocks-grid/dist/es/index.js 1.24 kB
packages/wonder-blocks-icon-button/dist/es/index.js 4.01 kB
packages/wonder-blocks-icon/dist/es/index.js 1.91 kB
packages/wonder-blocks-labeled-field/dist/es/index.js 3.47 kB
packages/wonder-blocks-layout/dist/es/index.js 1.63 kB
packages/wonder-blocks-link/dist/es/index.js 1.53 kB
packages/wonder-blocks-modal/dist/es/index.js 7.36 kB
packages/wonder-blocks-pill/dist/es/index.js 1.31 kB
packages/wonder-blocks-popover/dist/es/index.js 4.38 kB
packages/wonder-blocks-progress-spinner/dist/es/index.js 1.48 kB
packages/wonder-blocks-search-field/dist/es/index.js 1.11 kB
packages/wonder-blocks-styles/dist/es/index.js 464 B
packages/wonder-blocks-switch/dist/es/index.js 1.55 kB
packages/wonder-blocks-tabs/dist/es/index.js 5.65 kB
packages/wonder-blocks-testing-core/dist/es/index.js 3.25 kB
packages/wonder-blocks-testing/dist/es/index.js 978 B
packages/wonder-blocks-theming/dist/es/index.js 384 B
packages/wonder-blocks-timing/dist/es/index.js 1.37 kB
packages/wonder-blocks-tokens/dist/es/index.js 5.18 kB
packages/wonder-blocks-toolbar/dist/es/index.js 916 B
packages/wonder-blocks-tooltip/dist/es/index.js 6.02 kB
packages/wonder-blocks-typography/dist/es/index.js 1.58 kB

compressed-size-action

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 16, 2026

npm Snapshot: Published

🎉 Good news!! We've packaged up the latest commit from this PR (c1a8338) and published all packages with changesets to npm.

You can install the packages in frontend by running:

./dev/tools/deploy_wonder_blocks.js --tag="PR3015"

Packages can also be installed manually by running:

pnpm add @khanacademy/wonder-blocks-<package-name>@PR3015

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 16, 2026

A new build was pushed to Chromatic! 🚀

https://5e1bf4b385e3fb0020b7073c-juaikzgwqi.chromatic.com/

Chromatic results:

Metric Total
Captured snapshots 0
Tests with visual changes 0
Total stories 835
Inherited (not captured) snapshots [TurboSnap] 462
Tests on the build 462

@marcysutton marcysutton marked this pull request as draft April 16, 2026 23:15
@marcysutton marcysutton changed the title BodyText linting part 2: no-invalid-bodytext-children [Draft] BodyText linting part 2: no-invalid-bodytext-children Apr 16, 2026
@marcysutton marcysutton force-pushed the WB-2247-bodytext-lint branch 5 times, most recently from 4bc6115 to f554d3f Compare April 21, 2026 20:48
Base automatically changed from WB-2247-bodytext-lint to main April 21, 2026 23:24
@marcysutton marcysutton force-pushed the WB-2248-bodytext-children branch from 4199e63 to 5e7182a Compare April 30, 2026 18:57
@marcysutton marcysutton marked this pull request as ready for review April 30, 2026 23:06
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@marcysutton marcysutton changed the title [Draft] BodyText linting part 2: no-invalid-bodytext-children BodyText linting part 2: no-invalid-bodytext-children Apr 30, 2026
@marcysutton
Copy link
Copy Markdown
Member Author

Ok I'm marking this one for review to see if we want to keep it. There weren't any violations in frontend, so I'm not sure it's adding much value! But since I already had it implemented I figured we could discuss.

Here's the integration PR: https://github.com/Khan/frontend/pull/10738

@marcysutton
Copy link
Copy Markdown
Member Author

The navigation tabs playtesting stories failed again even though I updated from main 🤔 Odd!

@marcysutton
Copy link
Copy Markdown
Member Author

@claude review once

Comment thread .changeset/healthy-peas-push.md Outdated
Comment on lines +1 to +5
---
"@khanacademy/eslint-plugin-wonder-blocks": minor
---

Add new Typography lint rule for no-invalid-bodytext-parent to avoid nested paragraphs
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 This PR adds .changeset/healthy-peas-push.md with a description for the no-invalid-bodytext-parent rule, but that changeset was already consumed by Version Packages PR #3018 and shipped in @khanacademy/eslint-plugin-wonder-blocks@0.3.0. Re-introducing it here will cause a second minor version bump with a duplicate CHANGELOG entry for an already-released feature. Please delete .changeset/healthy-peas-push.md before merging — .changeset/brown-tips-draw.md is the correct changeset for this PR.

Extended reasoning...

What the bug is

.changeset/healthy-peas-push.md is being re-added in this PR with the body "Add new Typography lint rule for no-invalid-bodytext-parent to avoid nested paragraphs". This describes the previous Bodytext lint rule (the parent rule shipped in part 1), not the new no-invalid-bodytext-children rule that this PR introduces.

How it manifests

When this PR merges:

  1. The changesets bot will pick up both .changeset/brown-tips-draw.md (the legitimate one for this PR) and .changeset/healthy-peas-push.md (the stale duplicate).
  2. On the next Version Packages run, both changesets will be consumed and folded into one minor bump for @khanacademy/eslint-plugin-wonder-blocks.
  3. The resulting CHANGELOG entry will list both "Add new no-invalid-bodytext-children rule…" and "Add new Typography lint rule for no-invalid-bodytext-parent to avoid nested paragraphs" — but the second line refers to a feature that already shipped in v0.3.0, so consumers will see a misleading duplicate entry.

Step-by-step proof

  1. git log --diff-filter=A --all -- .changeset/healthy-peas-push.md shows the file was originally created in commit 8868c54 on the WB-2247 branch (the no-invalid-bodytext-parent rule).
  2. PR Introduce lint rule: no-invalid-bodytext-parent #2997 (commit a421ae1) merged that branch to main, bringing healthy-peas-push.md along.
  3. Version Packages PR Version Packages #3018 (commit e580a78) consumed and deleted .changeset/healthy-peas-push.md while publishing @khanacademy/eslint-plugin-wonder-blocks@0.3.0. packages/eslint-plugin-wonder-blocks/CHANGELOG.md line 7 records: "a421ae1: Add new Typography lint rule for no-invalid-bodytext-parent to avoid nested paragraphs" — byte-identical to the body of the changeset being re-added here.
  4. git show origin/main:.changeset/healthy-peas-push.md fails because the file no longer exists on main. The PR diff shows it being added from /dev/null, confirming it's a fresh re-introduction.

Why existing code doesn't prevent it

Changesets has no awareness of which descriptions have already shipped — it processes whatever .md files are present in .changeset/ at release time. The changeset-bot comment on this PR ("includes changesets to release 1 package" — Minor) reflects only that there is at least one minor changeset; it doesn't catch that one of them is stale.

Likely cause

This PR was opened on 2026-04-16, before #2997 merged. The branch likely picked up 8868c54 either directly or via rebase, and when the Version Packages PR later removed the file from main, the file remained on this feature branch.

How to fix

Simply delete .changeset/healthy-peas-push.md from this branch before merging. The legitimate changeset .changeset/brown-tips-draw.md already correctly describes the new no-invalid-bodytext-children rule and should be retained.

| Rule | Enabled in `recommended`| Enabled in `strict` |
|------|-------------------------|---------------------|
| [`no-custom-tab-role`](docs/no-custom-tab-role.md)| |✅|
| [`no-invalid-bodytext-children`](docs/no-invalid-bodytext-children.md)|✅| |
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 The 'recommended' status of no-invalid-bodytext-children is inconsistent across three places: the README rules table (README.md:74) marks it as ✅ in recommended and blank in strict, the rule's meta declares recommended: true (no-invalid-bodytext-children.ts:32), but src/configs/recommended.ts has rules: {} and the rule is only added to src/configs/strict.ts. So users extending plugin:@khanacademy/wonder-blocks/recommended will silently NOT get the rule, contrary to what the README documents. Easiest fix: swap the README ✅ to the strict column to match the actual config and follow the convention used by the other two rows in the table.

Extended reasoning...

What's wrong

There are three sources of truth for whether no-invalid-bodytext-children is part of the recommended preset, and they don't agree:

  1. README rules table (packages/eslint-plugin-wonder-blocks/README.md:74):
    | [`no-invalid-bodytext-children`](docs/no-invalid-bodytext-children.md)|✅| |
    
    ✅ in recommended, blank in strict.
  2. Rule meta (src/rules/no-invalid-bodytext-children.ts:32): recommended: true.
  3. Actual configs:
    • src/configs/recommended.tsrules: {} (empty)
    • src/configs/strict.ts:8'@khanacademy/wonder-blocks/no-invalid-bodytext-children': 'error'

So the rule is only enabled when consumers extend strict, but the README markets the recommended preset as the simpler entry point and tells readers this rule is in it.

Step-by-step proof a consumer is misled

  1. A consumer reads the README and decides on the lighter recommended preset:
    // .eslintrc.js
    module.exports = { extends: ['plugin:@khanacademy/wonder-blocks/recommended'] };
  2. They look at the rules table at line 74 and see no-invalid-bodytext-children ✅ under recommended. They reasonably conclude the rule will fire on their code.
  3. They write <BodyText><div>x</div></BodyText>.
  4. ESLint runs against the merged config from src/configs/recommended.ts, which has rules: {}. Nothing fires. No warning, no error.
  5. The same code under plugin:@khanacademy/wonder-blocks/strict would correctly produce the divChild error.

Why the existing code doesn't catch this

There's no automated check that the README table matches the configs, and the rule meta's recommended: true is used independently of the config exports. The other two rows in the table (no-custom-tab-role, no-invalid-bodytext-parent) follow the strict-only convention and happen to match the configs — so this row is the odd one out.

Addressing the refutation

A refutation noted that no-custom-tab-role.ts:19 also has recommended: true in meta despite being strict-only, suggesting this is an existing convention and the new rule simply follows it. That's a fair observation about meta.docs.recommended specifically — that field is somewhat decoupled from the actual config exports in this plugin already, so changing it isn't strictly required. However, it does not explain or justify the README discrepancy: no-custom-tab-role's row in the README table is correctly marked ✅ in strict and blank in recommended, matching its placement. The new rule's row inverts the columns relative to its actual placement, which is the clear, user-facing bug.

Impact

Documentation-only, but consequential: the README is what consumers use to choose between presets, and it currently misrepresents which preset enables this rule. Anyone following the docs to opt into the lighter recommended preset will silently miss this lint coverage.

How to fix

Pick one (the first is the smallest change and matches existing convention):

  • Option A (recommended): Swap the checkmarks in README.md:74 to | | ✅ | so the row matches strict-only placement, like the other two rows. Optionally also flip recommended: truerecommended: false in no-invalid-bodytext-children.ts:32 to match no-invalid-bodytext-parent.ts:101.
  • Option B: Add '@khanacademy/wonder-blocks/no-invalid-bodytext-children': 'error' to src/configs/recommended.ts so the configs match the README + meta. (This is a bigger behavior change for consumers of recommended.)

@marcysutton marcysutton force-pushed the WB-2248-bodytext-children branch from 5e7182a to 8253206 Compare May 1, 2026 16:33
@marcysutton marcysutton force-pushed the WB-2248-bodytext-children branch from 8253206 to 2a2725d Compare May 1, 2026 16:38
@beaesguerra
Copy link
Copy Markdown
Member

Ok I'm marking this one for review to see if we want to keep it. There weren't any violations in frontend, so I'm not sure it's adding much value! But since I already had it implemented I figured we could discuss.

Here's the integration PR: Khan/frontend#10738

I haven't reviewed the PR yet but I tried out the example in the demo project! I think it would be valuable to have still, so future instances can be prevented!

One thing I noticed is that there are cases where it would get lint errors for both rules:

image

Would it make sense to only report one of those rules in those cases?

I also found it interesting that the error squiggly would be on the child element in some cases! I was expecting that the parent element would be the one with the lint rule violation because it's the element with invalid children elements.

image

I'm also curious if there are any violations in the legacy apps. If not, we can add this rule to recommended so that instances of it can't be added in legacy apps too!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants