Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 18 additions & 0 deletions .changeset/div-deprecation-warn-once.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@object-ui/components": patch
---

The `div` deprecation notice is now reported once per module load, not once per render (objectui#3965)

`DivRenderer` called `console.warn` on every render. That is invisible on a page
with one `div` and destructive on a page with many: the docs schema-catalog index
renders 400+ example thumbnails and emitted ~190 byte-identical notices, burying
the page's real console errors underneath — the two nested-button errors fixed in
objectui#3903 / PR #3964 had to be fished out of that flood, and it cost a
browser-verification run its signal twice.

The deprecation itself is unchanged: dev builds still report it, the message and
its migration guidance are byte-identical, and production builds are still
silent. Only the repetition is gone. The guard is a module-level `Set` keyed by
type, and the production early-return happens *before* the set is marked, so a
production render cannot suppress a later dev-build notice.
103 changes: 103 additions & 0 deletions packages/components/src/__tests__/div-deprecation-warn-once.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* Pins the once-semantics of the `div` deprecation notice (objectui#3965).
*
* `DivRenderer` used to `console.warn` on EVERY render. That is harmless on a
* page with one `div`, and destructive on a page with many: the docs
* schema-catalog index renders 400+ example thumbnails and emitted ~190
* identical notices, which buried the page's real console errors underneath —
* it twice cost a browser-verification run the signal it was looking for
* (#3903 / PR #3964 had to fish two nested-button errors out of the flood).
*
* What is pinned here is the DEDUPLICATION, not the removal of the warning:
* the deprecation still fires in dev builds, exactly once per module load.
*
* NOTE ON ORDER — the guard is a module-level Set, so it latches for the
* lifetime of this module instance. The production-silence case must therefore
* run BEFORE the dev case, and it doubles as a pin on the guard's ordering:
* the production early-return happens before the Set is marked, so a
* production render must not swallow a later dev notice.
*/

import { describe, it, expect, vi, afterEach } from 'vitest';
import { render } from '@testing-library/react';
import { ComponentRegistry } from '@object-ui/core';
// Registers the renderers at module scope, NOT inside a `beforeAll` — there the
// cold transform is billed to `hookTimeout`. See
// object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021).
import '../renderers';

const DEPRECATION_RE = /The "div" component is deprecated/;

function renderDiv(schema: Record<string, unknown>) {
const Component = ComponentRegistry.get('div');
if (!Component) throw new Error('Component "div" is not registered');
return render(<Component schema={schema} />);
}

function deprecationCalls(spy: ReturnType<typeof vi.spyOn>): unknown[][] {
return spy.mock.calls.filter((args) => DEPRECATION_RE.test(String(args[0])));
}

describe('div deprecation notice — once per module load (#3965)', () => {
afterEach(() => {
vi.unstubAllEnvs();
vi.restoreAllMocks();
});

// MUST run first: see the note above. A production render may not mark the
// warn-once Set, otherwise it would suppress the dev notice that follows.
it('stays silent in production builds', () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
vi.stubEnv('NODE_ENV', 'production');

renderDiv({ type: 'div', className: 'p-4', children: [{ type: 'text', content: 'a' }] });

expect(deprecationCalls(warn)).toHaveLength(0);
});

it('warns exactly once however many div nodes render', () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});

// Three separate renders, and one of them nests three more `div` nodes:
// seven DivRenderer invocations in total, one notice expected.
renderDiv({ type: 'div', className: 'a' });
renderDiv({ type: 'div', className: 'b' });
renderDiv({
type: 'div',
className: 'outer',
children: [
{
type: 'div',
className: 'middle',
children: [
{ type: 'div', className: 'inner-1' },
{ type: 'div', className: 'inner-2' },
],
},
],
});

const calls = deprecationCalls(warn);
expect(calls).toHaveLength(1);
// The notice itself is unchanged — the deprecation is still being reported,
// with its migration guidance intact.
expect(String(calls[0][0])).toContain('"card", "flex", or semantic layout components');
expect(String(calls[0][0])).toContain('"container", "stack", or "grid"');
});

it('does not warn again on a later render', () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});

renderDiv({ type: 'div', className: 'later' });

expect(deprecationCalls(warn)).toHaveLength(0);
});
});
46 changes: 36 additions & 10 deletions packages/components/src/renderers/basic/div.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,44 @@ import type { DivSchema } from '@object-ui/types';
import { renderChildren } from '../../lib/utils';
import { forwardRef } from 'react';

/**
* Deprecated types already reported in this module instance — the notice is a
* property of the TYPE, not of the node, so one report per page load is the
* whole signal. Mirrors the warn-once machinery in `layout/containers.tsx`.
*/
const _warnedDeprecations = new Set<string>();

/**
* Report the deprecation ONCE per type per module load.
*
* The renderer used to warn on every single render, which turns any page with
* many `div` nodes into a console flood: the docs schema-catalog index renders
* 400+ example thumbnails and produced ~190 identical notices, burying the
* real errors underneath (it twice cost a browser-verification run the signal
* it was looking for — objectui#3965, discovered during #3903 / PR #3964).
*
* The deprecation itself is unchanged and still fires in dev builds; only the
* repetition is dropped. Order matters: the production check returns BEFORE the
* seen-set is marked, so a production render never suppresses the dev notice.
*/
function warnDeprecatedOnce(type: string, message: string): void {
if (process.env.NODE_ENV === 'production') return;
if (_warnedDeprecations.has(type)) return;
_warnedDeprecations.add(type);
console.warn(message);
}

const DivRenderer = forwardRef<HTMLDivElement, { schema: DivSchema; className?: string; [key: string]: any }>(
({ schema, className, ...props }, ref) => {
// Deprecation warning
if (process.env.NODE_ENV !== 'production') {
console.warn(
'[ObjectUI] The "div" component is deprecated. Please use Shadcn components instead:\n' +
' - For containers: use "card", "flex", or semantic layout components\n' +
' - For simple wrappers: use layout components like "container", "stack", or "grid"\n' +
'See documentation at https://www.objectui.org/docs/components for alternatives.'
);
}

// Deprecation warning (once per module load — see warnDeprecatedOnce)
warnDeprecatedOnce(
'div',
'[ObjectUI] The "div" component is deprecated. Please use Shadcn components instead:\n' +
' - For containers: use "card", "flex", or semantic layout components\n' +
' - For simple wrappers: use layout components like "container", "stack", or "grid"\n' +
'See documentation at https://www.objectui.org/docs/components for alternatives.'
);

// Extract designer-related props
const {
'data-obj-id': dataObjId,
Expand Down
Loading