-
Notifications
You must be signed in to change notification settings - Fork 256
feat(dev-validation): add reusable dev-mode validation helpers and docs #6508
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
rubencarvalho
wants to merge
28
commits into
main
Choose a base branch
from
ruben/feat-dev-warning-validation-foundation
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,204
−46
Open
Changes from 13 commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
67fe2d4
feat(dev-validation): add reusable dev-mode validation helpers and docs
rubencarvalho bf43016
docs(dev-validation): say Gen2 instead of 2nd-gen in the consumer guide
rubencarvalho 7e1a810
docs(dev-validation): add per-bundler production-stripping recipes
rubencarvalho e7f6de9
feat(dev-validation): gate helpers on the literal NODE_ENV, not just …
rubencarvalho 2b02150
docs(dev-validation): trim over-justification of the residual call-si…
rubencarvalho 94904f4
fix(dev-warning): remove em-dash
miwha-adobe ba43186
refactor(dev-validation): centralize the dev warn gate in emitWarning
miwha-adobe 402a757
Merge branch 'main' of https://github.com/adobe/spectrum-web-componen…
miwha-adobe ab817c4
test(dev-validation): cover DEBUG gate, dedup key, and helper edge cases
miwha-adobe 0939a81
test(dev-validation): cover DEBUG gate, dedup key, and helper edge ca…
miwha-adobe 75bd986
fix(debug-validation): prettier format char width limit
miwha-adobe 0ab5a9f
fix(dev-warning): additional text wrapping
miwha-adobe ce10af4
fix(docs): update the known limitations to be less verbose
miwha-adobe b45b7b3
Merge branch 'main' into ruben/feat-dev-warning-validation-foundation
miwha-adobe 0532ec5
Merge branch 'main' of https://github.com/adobe/spectrum-web-componen…
miwha-adobe ca3d45a
Merge branch 'main' of https://github.com/adobe/spectrum-web-componen…
miwha-adobe f4f6ad4
Merge branch 'ruben/feat-dev-warning-validation-foundation' of https:…
miwha-adobe e37cd37
feat(dev-validation): add isDebug() gate for expensive call-site checks
miwha-adobe cf4efb8
test(dev-warning): add isDebug test
miwha-adobe 5c1c5ce
chore(dev-warning): add group warnings and a count
miwha-adobe ae96d85
fix(dev-warning): refine conditional to be exclusive
miwha-adobe 397d9ec
Merge branch 'main' into ruben/feat-dev-warning-validation-foundation
miwha-adobe 4efeab9
Merge branch 'main' of https://github.com/adobe/spectrum-web-componen…
miwha-adobe 3c07ec7
Merge branch 'main' of https://github.com/adobe/spectrum-web-componen…
miwha-adobe ca302e2
Merge branch 'main' of https://github.com/adobe/spectrum-web-componen…
miwha-adobe 894fe13
fix(dev-validation): count distinct elements in grouped warnings
miwha-adobe 0a86378
Merge branch 'main' into ruben/feat-dev-warning-validation-foundation
miwha-adobe c132f5e
Merge branch 'main' of https://github.com/adobe/spectrum-web-componen…
miwha-adobe File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| '@spectrum-web-components/core': minor | ||
| --- | ||
|
|
||
| **feat(dev-validation):** Added reusable dev-mode validation helpers (`validateEnum`, `warnIf`, `validateRequiredSlot`, `validateAllowedChildren`) in `@spectrum-web-components/core/utils`, and fixed `window.__swc.warn`'s dedup key so two distinct warnings on the same component no longer suppress each other. | ||
|
|
||
| Component authors should use these helpers instead of hand-rolled `includes()` + `window.__swc.warn()` checks for union/enum values, required and conditionally required properties, mutually exclusive/no-effect combinations, required slots, and allowed slotted children. See the "Reusable validation helpers" and "Slot validation" sections of the [Debug and validation style guide](https://github.com/adobe/spectrum-web-components/blob/main/CONTRIBUTOR-DOCS/02_style-guide/02_typescript/17_debug-validation.md). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -68,6 +68,22 @@ export class SpectrumElement extends SpectrumMixin(LitElement) { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Builds the deduplication key for a dev-mode warning. The `message` is part of | ||
| * the key so two distinct problems that share a `type`/`level` (the common | ||
| * case) do not suppress each other; only a verbatim repeat of the same warning | ||
| * is deduplicated. Exported so the dedup key can be unit tested without relying | ||
| * on the `NODE_ENV`-gated `window.__swc` setup below. | ||
| */ | ||
| export function warningId( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Extracted warningId so the dedup-key fix could be unit tested |
||
| localName: string, | ||
| type: WarningType, | ||
| level: WarningLevel, | ||
| message: string | ||
| ): BrandedSWCWarningID { | ||
| return `${localName}:${type}:${level}:${message}` as BrandedSWCWarningID; | ||
| } | ||
|
|
||
| if (process.env.NODE_ENV === 'development') { | ||
|
miwha-adobe marked this conversation as resolved.
Outdated
|
||
| const ignoreWarningTypes = { | ||
| default: false, | ||
|
|
@@ -103,7 +119,7 @@ if (process.env.NODE_ENV === 'development') { | |
| { type = 'api', level = 'default', issues } = {} | ||
| ): void => { | ||
| const { localName = 'base' } = element || {}; | ||
| const id = `${localName}:${type}:${level}` as BrandedSWCWarningID; | ||
| const id = warningId(localName, type, level, message); | ||
| if (!window.__swc.verbose && window.__swc.issuedWarnings.has(id)) { | ||
| return; | ||
| } | ||
|
|
||
55 changes: 55 additions & 0 deletions
55
2nd-gen/packages/core/element/test/spectrum-element.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| /** | ||
| * Copyright 2026 Adobe. All rights reserved. | ||
| * This file is licensed to you under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. You may obtain a copy | ||
| * of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software distributed under | ||
| * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS | ||
| * OF ANY KIND, either express or implied. See the License for the specific language | ||
| * governing permissions and limitations under the License. | ||
| */ | ||
|
|
||
| import { html } from 'lit'; | ||
| import { expect } from '@storybook/test'; | ||
| import type { Meta, StoryObj as Story } from '@storybook/web-components'; | ||
|
|
||
| import { warningId } from '../spectrum-element.js'; | ||
|
|
||
| export default { | ||
| title: 'Utils/Dev validation/Dedup tests', | ||
| tags: ['!autodocs', 'dev'], | ||
| render: () => html` | ||
| <div></div> | ||
| `, | ||
| } as Meta; | ||
|
|
||
| // `warningId` builds the dedup key `window.__swc.warn` uses to suppress repeat | ||
| // warnings. Testing it directly guards the dedup-key fix (adding `message` to | ||
| // the key) without depending on the `NODE_ENV`-gated `window.__swc` setup, | ||
| // which the test harness (`NODE_ENV === 'test'`) never runs. | ||
| export const WarningIdTest: Story = { | ||
| play: async ({ step }) => { | ||
| await step('distinct messages produce distinct dedup keys', () => { | ||
| const first = warningId('swc-badge', 'api', 'default', 'first problem'); | ||
| const second = warningId('swc-badge', 'api', 'default', 'second problem'); | ||
| // Same localName/type/level, different message: the keys must differ so | ||
| // the second warning is not suppressed. This is the behavior the | ||
| // dedup-key fix restored. | ||
| expect(first).not.toBe(second); | ||
| }); | ||
|
|
||
| await step('an identical warning produces the same dedup key', () => { | ||
| const first = warningId('swc-badge', 'api', 'default', 'same problem'); | ||
| const second = warningId('swc-badge', 'api', 'default', 'same problem'); | ||
| // A verbatim repeat collapses to one key, so it is deduplicated. | ||
| expect(first).toBe(second); | ||
| }); | ||
|
|
||
| await step('the key is localName:type:level:message, in that order', () => { | ||
| expect(String(warningId('swc-badge', 'api', 'high', 'bad variant'))).toBe( | ||
| 'swc-badge:api:high:bad variant' | ||
| ); | ||
| }); | ||
| }, | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| /** | ||
| * Copyright 2026 Adobe. All rights reserved. | ||
| * This file is licensed to you under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. You may obtain a copy | ||
| * of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software distributed under | ||
| * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS | ||
| * OF ANY KIND, either express or implied. See the License for the specific language | ||
| * governing permissions and limitations under the License. | ||
| */ | ||
|
|
||
| /** | ||
| * The single place "warn in development" lives: the `DEBUG` gate plus the | ||
| * `window.__swc.warn` call. Every public helper below computes its own | ||
| * predicate and message, then defers the actual emit to this primitive, so | ||
| * there is one source of truth for how a warning is gated and dispatched. | ||
| * | ||
| * Note: this deliberately does **not** include the | ||
| * `process.env.NODE_ENV === 'production'` guard. That guard must stay in each | ||
| * public helper so a bundler can dead-code-eliminate the message construction | ||
| * inside that function for production builds; centralizing it here would leave | ||
| * each caller's message-building in the production bundle. | ||
| * | ||
| * @param element - The component instance the warning is attributed to. | ||
| * @param message - The warning message. | ||
| * @param url - Documentation URL for the component. | ||
| * @param options - Passed through to `window.__swc.warn`. | ||
| */ | ||
| function emitWarning( | ||
| element: HTMLElement, | ||
| message: string, | ||
| url: string, | ||
| options?: SWCWarningOptions | ||
| ): void { | ||
| if (!window.__swc?.DEBUG) { | ||
| return; | ||
| } | ||
| window.__swc.warn(element, message, url, options); | ||
| } | ||
|
|
||
| /** | ||
| * Warns when `value` is not one of `valid`. Covers union-type/enum property | ||
| * validation (e.g. `variant`, `size`). | ||
| * | ||
| * @param element - The component instance the warning is attributed to. | ||
| * @param check - The enum check to perform. | ||
| * @param check.prop - The property name, for the warning message. | ||
| * @param check.value - The value received. | ||
| * @param check.valid - The allowed values. | ||
| * @param check.url - Documentation URL for the component. | ||
| * @param check.options - Passed through to `window.__swc.warn`. | ||
| */ | ||
| export function validateEnum<T extends string>( | ||
| element: HTMLElement, | ||
| { | ||
| prop, | ||
| value, | ||
| valid, | ||
| url, | ||
| options, | ||
| }: { | ||
| prop: string; | ||
| value: string; | ||
| valid: readonly T[]; | ||
| url: string; | ||
| options?: SWCWarningOptions; | ||
| } | ||
| ): void { | ||
| if (process.env.NODE_ENV === 'production') { | ||
| return; | ||
| } | ||
| if ((valid as readonly string[]).includes(value)) { | ||
| return; | ||
| } | ||
| emitWarning( | ||
| element, | ||
| `<${element.localName}> expects "${prop}" to be one of: ${valid.join(', ')}. Received "${value}".`, | ||
| url, | ||
| { issues: [`${prop}="${value}"`], ...options } | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Warns when `condition` is true. The general-purpose validation primitive: | ||
| * covers required properties, conditionally required properties, mutually | ||
| * exclusive/no-effect property combinations, and any component-specific | ||
| * quirk that doesn't fit the other helpers here. | ||
| * | ||
| * @param element - The component instance the warning is attributed to. | ||
| * @param condition - Warn when this is true. | ||
| * @param message - The warning message. | ||
| * @param url - Documentation URL for the component. | ||
| * @param options - Passed through to `window.__swc.warn`. | ||
| */ | ||
| export function warnIf( | ||
| element: HTMLElement, | ||
| condition: boolean, | ||
| message: string, | ||
| url: string, | ||
| options?: SWCWarningOptions | ||
| ): void { | ||
| if (process.env.NODE_ENV === 'production') { | ||
| return; | ||
| } | ||
| if (!condition) { | ||
| return; | ||
| } | ||
| emitWarning(element, message, url, options); | ||
| } | ||
|
|
||
| /** | ||
| * Warns when a slot has no assigned nodes. Covers required-slot validation. | ||
| * | ||
| * @param element - The component instance the warning is attributed to. | ||
| * @param slot - The slot element to check (`null`/`undefined` counts as empty). | ||
| * @param slotName - The slot's `name` attribute (or `"default"`), for the message. | ||
| * @param url - Documentation URL for the component. | ||
| * @param options - Passed through to `window.__swc.warn`. | ||
| */ | ||
| export function validateRequiredSlot( | ||
| element: HTMLElement, | ||
| slot: HTMLSlotElement | null | undefined, | ||
| slotName: string, | ||
| url: string, | ||
| options?: SWCWarningOptions | ||
| ): void { | ||
| if (process.env.NODE_ENV === 'production') { | ||
| return; | ||
| } | ||
| const isEmpty = !slot || slot.assignedNodes({ flatten: true }).length === 0; | ||
| if (!isEmpty) { | ||
| return; | ||
| } | ||
| emitWarning( | ||
| element, | ||
| `<${element.localName}> requires content in the "${slotName}" slot.`, | ||
| url, | ||
| options | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Warns for each assigned element in `slot` whose tag name is not in | ||
| * `allowedTagNames`. Covers allowed-children slot validation (e.g. a heading | ||
| * slot that only accepts `<h2>`-`<h6>`). | ||
| * | ||
| * @param element - The component instance the warning is attributed to. | ||
| * @param slot - The slot element to check. | ||
| * @param allowedTagNames - Allowed tag names (case-insensitive, e.g. `['h2', 'h3']`). | ||
| * @param slotName - The slot's `name` attribute (or `"default"`), for the message. | ||
| * @param url - Documentation URL for the component. | ||
| * @param options - Passed through to `window.__swc.warn`. | ||
| */ | ||
| export function validateAllowedChildren( | ||
| element: HTMLElement, | ||
| slot: HTMLSlotElement | null | undefined, | ||
| allowedTagNames: readonly string[], | ||
| slotName: string, | ||
| url: string, | ||
| options?: SWCWarningOptions | ||
| ): void { | ||
| if (process.env.NODE_ENV === 'production') { | ||
| return; | ||
| } | ||
| if (!slot) { | ||
| return; | ||
| } | ||
| const allowed = allowedTagNames.map((tag) => tag.toUpperCase()); | ||
| const allowedList = allowedTagNames.map((tag) => `<${tag}>`).join(', '); | ||
| for (const el of slot.assignedElements()) { | ||
| if (!allowed.includes(el.tagName)) { | ||
| emitWarning( | ||
|
miwha-adobe marked this conversation as resolved.
|
||
| element, | ||
| `<${element.localName}> "${slotName}" slot received a <${el.tagName.toLowerCase()}> element. Only ${allowedList} elements are allowed.`, | ||
| url, | ||
| { | ||
| issues: [`${slotName} slot: <${el.tagName.toLowerCase()}>`], | ||
| ...options, | ||
| } | ||
| ); | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure where this came from. Is this the right call to update this version to
2.0.0-beta.2? Our latest release wasSince 2.0.0-beta.1There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we actually should make a ticket to remove this and disable the script thats updating it. its no longer coupled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed here: #6510