Skip to content
Open
Show file tree
Hide file tree
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 Jul 14, 2026
bf43016
docs(dev-validation): say Gen2 instead of 2nd-gen in the consumer guide
rubencarvalho Jul 15, 2026
7e1a810
docs(dev-validation): add per-bundler production-stripping recipes
rubencarvalho Jul 15, 2026
e7f6de9
feat(dev-validation): gate helpers on the literal NODE_ENV, not just …
rubencarvalho Jul 15, 2026
2b02150
docs(dev-validation): trim over-justification of the residual call-si…
rubencarvalho Jul 15, 2026
94904f4
fix(dev-warning): remove em-dash
miwha-adobe Jul 23, 2026
ba43186
refactor(dev-validation): centralize the dev warn gate in emitWarning
miwha-adobe Jul 24, 2026
402a757
Merge branch 'main' of https://github.com/adobe/spectrum-web-componen…
miwha-adobe Jul 24, 2026
ab817c4
test(dev-validation): cover DEBUG gate, dedup key, and helper edge cases
miwha-adobe Jul 24, 2026
0939a81
test(dev-validation): cover DEBUG gate, dedup key, and helper edge ca…
miwha-adobe Jul 24, 2026
75bd986
fix(debug-validation): prettier format char width limit
miwha-adobe Jul 24, 2026
0ab5a9f
fix(dev-warning): additional text wrapping
miwha-adobe Jul 24, 2026
ce10af4
fix(docs): update the known limitations to be less verbose
miwha-adobe Jul 24, 2026
b45b7b3
Merge branch 'main' into ruben/feat-dev-warning-validation-foundation
miwha-adobe Jul 24, 2026
0532ec5
Merge branch 'main' of https://github.com/adobe/spectrum-web-componen…
miwha-adobe Jul 24, 2026
ca3d45a
Merge branch 'main' of https://github.com/adobe/spectrum-web-componen…
miwha-adobe Jul 24, 2026
f4f6ad4
Merge branch 'ruben/feat-dev-warning-validation-foundation' of https:…
miwha-adobe Jul 24, 2026
e37cd37
feat(dev-validation): add isDebug() gate for expensive call-site checks
miwha-adobe Jul 27, 2026
cf4efb8
test(dev-warning): add isDebug test
miwha-adobe Jul 27, 2026
5c1c5ce
chore(dev-warning): add group warnings and a count
miwha-adobe Jul 29, 2026
ae96d85
fix(dev-warning): refine conditional to be exclusive
miwha-adobe Jul 29, 2026
397d9ec
Merge branch 'main' into ruben/feat-dev-warning-validation-foundation
miwha-adobe Jul 29, 2026
4efeab9
Merge branch 'main' of https://github.com/adobe/spectrum-web-componen…
miwha-adobe Aug 3, 2026
3c07ec7
Merge branch 'main' of https://github.com/adobe/spectrum-web-componen…
miwha-adobe Aug 7, 2026
ca302e2
Merge branch 'main' of https://github.com/adobe/spectrum-web-componen…
miwha-adobe Aug 10, 2026
894fe13
fix(dev-validation): count distinct elements in grouped warnings
miwha-adobe Aug 10, 2026
0a86378
Merge branch 'main' into ruben/feat-dev-warning-validation-foundation
miwha-adobe Aug 10, 2026
c132f5e
Merge branch 'main' of https://github.com/adobe/spectrum-web-componen…
miwha-adobe Aug 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .ai/rules/code-conformance.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Reference: [Linting tools](../../CONTRIBUTOR-DOCS/02_style-guide/03_linting-tool
- Methods follow visibility and naming conventions
- JSDoc is present and well-formed on public API members
- No patterns listed as anti-patterns or discouraged in the guide
- Dev-warning validation (enum values, required/conditionally required properties, mutually exclusive combinations, required slots, allowed children) uses the shared helpers in `@spectrum-web-components/core/utils` (`validateEnum`, `warnIf`, `validateRequiredSlot`, `validateAllowedChildren`), not hand-rolled `includes()` + `window.__swc.warn()` checks. See [Debug and validation](../../CONTRIBUTOR-DOCS/02_style-guide/02_typescript/17_debug-validation.md#reusable-validation-helpers).

## CSS

Expand Down
7 changes: 7 additions & 0 deletions .changeset/dev-warning-validation-helpers.md
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).
2 changes: 1 addition & 1 deletion 1st-gen/tools/base/src/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ export const version = '1.12.2';
/**
* The version of the core base package.
*/
export const coreVersion = '0.3.0';
export const coreVersion = '2.0.0-beta.2';

Copy link
Copy Markdown
Contributor

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 was Since 2.0.0-beta.1

Copy link
Copy Markdown
Contributor

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fixed here: #6510

18 changes: 17 additions & 1 deletion 2nd-gen/packages/core/element/spectrum-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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') {
Comment thread
miwha-adobe marked this conversation as resolved.
Outdated
const ignoreWarningTypes = {
default: false,
Expand Down Expand Up @@ -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;
}
Expand Down
55 changes: 55 additions & 0 deletions 2nd-gen/packages/core/element/test/spectrum-element.test.ts
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'
);
});
},
};
3 changes: 2 additions & 1 deletion 2nd-gen/packages/core/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ type SWCWarningOptions = {
level?: WarningLevel;
issues?: string[];
};
type BrandedSWCWarningID = `${ElementLocalName}:${WarningType}:${WarningLevel}`;
type BrandedSWCWarningID =
`${ElementLocalName}:${WarningType}:${WarningLevel}:${string}`;

/**
* ARIA element-reflection properties (IDRef-free associations). Not yet in the
Expand Down
184 changes: 184 additions & 0 deletions 2nd-gen/packages/core/utils/dev-validation.ts
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(
Comment thread
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,
}
);
}
}
}
6 changes: 6 additions & 0 deletions 2nd-gen/packages/core/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
export { physicalSide } from './actual-placement.js';
export { capitalize } from './capitalize.js';
export { deepContains } from './deep-contains.js';
export {
validateAllowedChildren,
validateEnum,
validateRequiredSlot,
warnIf,
} from './dev-validation.js';
export {
isTopDismissible,
registerDismissible,
Expand Down
Loading
Loading