From e1cb3e7122c6bbcdb5181d9664de77b9e601d75f Mon Sep 17 00:00:00 2001 From: yinlianghui Date: Sun, 9 Aug 2026 22:27:45 +0000 Subject: [PATCH] =?UTF-8?q?fix(components):=20div=20=E5=BA=9F=E5=BC=83?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E6=AF=8F=E6=AC=A1=E6=A8=A1=E5=9D=97=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E5=8F=AA=E6=8A=A5=E4=B8=80=E6=AC=A1,=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E9=80=90=E6=AC=A1=E6=B8=B2=E6=9F=93=E5=88=B7=E5=B1=8F?= =?UTF-8?q?=20(#3965)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `DivRenderer` 原先在每一次渲染里 `console.warn`。单个 `div` 的页面看不出问题, `div` 多的页面就被刷成噪声墙:docs 的 schema-catalog 索引页渲染 400+ 个示例缩略图, 实测刷出约 190 条完全相同的提示,把页面真正的报错埋在下面 —— #3903 / PR #3964 的两条 嵌套按钮报错就是从这堆噪声里捞出来的,浏览器实证也因此两次丢掉信号。 废弃本身一字未动:dev 构建照旧提示,文案与迁移建议不变,production 构建照旧静默, 去掉的只是重复。守卫是模块级 `Set`(按 type 记),production 的提前返回发生在标记之前, 所以一次 production 渲染不会吞掉后续 dev 构建的提示。 反向验证(先预判后执行):把无守卫的旧写法改回去,「production 静默」用例按预期保持 绿(旧写法同样按 NODE_ENV 门控),「只报一次」与「后续渲染不再报」两条按预期翻红 (9 条提示 vs 期望 1 条;后续渲染多出 1 条)。 注:本 PR 只完成 #3965 的第二半(warn 去重)。第一半(catalog 48 个示例换掉废弃的 `div`)在现有组件词表下无法完整完成,已在 issue 上以证据升级给维护者定夺,见 PR 正文。 Co-Authored-By: Claude --- .changeset/div-deprecation-warn-once.md | 18 +++ .../div-deprecation-warn-once.test.tsx | 103 ++++++++++++++++++ .../components/src/renderers/basic/div.tsx | 46 ++++++-- 3 files changed, 157 insertions(+), 10 deletions(-) create mode 100644 .changeset/div-deprecation-warn-once.md create mode 100644 packages/components/src/__tests__/div-deprecation-warn-once.test.tsx diff --git a/.changeset/div-deprecation-warn-once.md b/.changeset/div-deprecation-warn-once.md new file mode 100644 index 0000000000..60992a7348 --- /dev/null +++ b/.changeset/div-deprecation-warn-once.md @@ -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. diff --git a/packages/components/src/__tests__/div-deprecation-warn-once.test.tsx b/packages/components/src/__tests__/div-deprecation-warn-once.test.tsx new file mode 100644 index 0000000000..58b92ffc12 --- /dev/null +++ b/packages/components/src/__tests__/div-deprecation-warn-once.test.tsx @@ -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) { + const Component = ComponentRegistry.get('div'); + if (!Component) throw new Error('Component "div" is not registered'); + return render(); +} + +function deprecationCalls(spy: ReturnType): 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); + }); +}); diff --git a/packages/components/src/renderers/basic/div.tsx b/packages/components/src/renderers/basic/div.tsx index 020051b67f..e622b1a1df 100644 --- a/packages/components/src/renderers/basic/div.tsx +++ b/packages/components/src/renderers/basic/div.tsx @@ -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(); + +/** + * 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( ({ 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,