diff --git a/packages/@ember/-internals/glimmer/lib/debug-render-tree-factory.ts b/packages/@ember/-internals/glimmer/lib/debug-render-tree-factory.ts new file mode 100644 index 00000000000..cac6fecf000 --- /dev/null +++ b/packages/@ember/-internals/glimmer/lib/debug-render-tree-factory.ts @@ -0,0 +1,29 @@ +import type { DebugRenderTree } from '@glimmer/interfaces'; + +/** + * The `DebugRenderTree` implementation is relatively heavy and is only needed + * when debug tooling (e.g. Ember Inspector) is in use. To allow the + * implementation to be tree-shaken out of production builds that never use + * it, the environment does not reference the implementation directly. + * Instead, an implementation is registered here: + * + * - in debug builds, eagerly (see `./environment`), preserving the existing + * behavior of `EmberENV._DEBUG_RENDER_TREE` defaulting to on in + * development; and + * - in production builds, as a side effect of importing + * `@ember/debug/lib/capture-render-tree` (the entry point Ember Inspector + * relies on), preserving the documented `EmberENV._DEBUG_RENDER_TREE` + * production opt-in for apps that include that module. + * + * Production apps whose module graph never reaches `captureRenderTree` pay + * neither the bytes nor the runtime cost. + */ +let debugRenderTreeFactory: (() => DebugRenderTree) | undefined; + +export function setDebugRenderTreeFactory(factory: () => DebugRenderTree): void { + debugRenderTreeFactory = factory; +} + +export function createDebugRenderTree(): DebugRenderTree | undefined { + return debugRenderTreeFactory?.(); +} diff --git a/packages/@ember/-internals/glimmer/lib/environment.ts b/packages/@ember/-internals/glimmer/lib/environment.ts index fc2d99f4f4d..6198040bd54 100644 --- a/packages/@ember/-internals/glimmer/lib/environment.ts +++ b/packages/@ember/-internals/glimmer/lib/environment.ts @@ -11,6 +11,8 @@ import { DEBUG } from '@glimmer/env'; import setGlobalContext from '@glimmer/global-context'; import type { EnvironmentDelegate } from '@glimmer/runtime/lib/environment'; import { debug } from '@glimmer/validator/lib/debug'; +import { createDebugRenderTree } from './debug-render-tree-factory'; +import { enableDebugRenderTree } from './setup-debug-render-tree'; import toIterator from './utils/iterator'; import { isHTMLSafe } from './utils/string'; import toBool from './utils/to-bool'; @@ -125,8 +127,17 @@ const VM_ASSERTION_OVERRIDES: { id: string; message: string }[] = []; // Define environment delegate +if (DEBUG) { + // In debug builds the render tree is always available (`_DEBUG_RENDER_TREE` + // defaults to on in development). In production builds this block is + // stripped, so the `DebugRenderTree` implementation is only included if + // something else imports it — notably `@ember/debug/lib/capture-render-tree`, + // which registers it for the `EmberENV._DEBUG_RENDER_TREE` opt-in. + enableDebugRenderTree(); +} + export class EmberEnvironmentDelegate implements EnvironmentDelegate { - public enableDebugTooling: boolean = ENV._DEBUG_RENDER_TREE; + public debugRenderTree = ENV._DEBUG_RENDER_TREE ? createDebugRenderTree() : undefined; constructor( public owner: InternalOwner, diff --git a/packages/@ember/-internals/glimmer/lib/setup-debug-render-tree.ts b/packages/@ember/-internals/glimmer/lib/setup-debug-render-tree.ts new file mode 100644 index 00000000000..3cb9e61ba70 --- /dev/null +++ b/packages/@ember/-internals/glimmer/lib/setup-debug-render-tree.ts @@ -0,0 +1,15 @@ +import DebugRenderTreeImpl from '@glimmer/runtime/lib/debug-render-tree'; + +import { setDebugRenderTreeFactory } from './debug-render-tree-factory'; + +/** + * Registers the real `DebugRenderTree` implementation so that renderers + * created afterwards will record the render tree (when + * `EmberENV._DEBUG_RENDER_TREE` is enabled). + * + * Importing this module is what causes the `DebugRenderTree` implementation + * to be included in a build; see `./debug-render-tree-factory`. + */ +export function enableDebugRenderTree(): void { + setDebugRenderTreeFactory(() => new DebugRenderTreeImpl()); +} diff --git a/packages/@ember/debug/lib/capture-render-tree.ts b/packages/@ember/debug/lib/capture-render-tree.ts index 03c5cd4b3fa..46092b80ddf 100644 --- a/packages/@ember/debug/lib/capture-render-tree.ts +++ b/packages/@ember/debug/lib/capture-render-tree.ts @@ -1,7 +1,15 @@ +import { enableDebugRenderTree } from '@ember/-internals/glimmer/lib/setup-debug-render-tree'; import type { Renderer } from '@ember/-internals/glimmer/lib/renderer'; import type Owner from '@ember/owner'; import type { CapturedRenderNode } from '@glimmer/interfaces'; +// Register the DebugRenderTree implementation as a side effect of importing +// this module. This is what makes `EmberENV._DEBUG_RENDER_TREE` work in +// production builds: any build that includes `captureRenderTree` (e.g. for +// Ember Inspector) also includes the render tree bookkeeping, while builds +// that never import it can tree-shake the implementation away entirely. +enableDebugRenderTree(); + /** @module @ember/debug */ diff --git a/packages/@glimmer-workspace/integration-tests/lib/base-env.ts b/packages/@glimmer-workspace/integration-tests/lib/base-env.ts index 24dc0280253..41412070345 100644 --- a/packages/@glimmer-workspace/integration-tests/lib/base-env.ts +++ b/packages/@glimmer-workspace/integration-tests/lib/base-env.ts @@ -20,8 +20,6 @@ export function scheduleDidDestroy(fn: () => void) { export const BaseEnv: EnvironmentDelegate = { isInteractive: true, - enableDebugTooling: false, - onTransactionCommit() { for (const { destroyable, destructor } of scheduled) { destructor(destroyable); diff --git a/packages/@glimmer-workspace/integration-tests/test/debug-render-tree-test.ts b/packages/@glimmer-workspace/integration-tests/test/debug-render-tree-test.ts index 90d4bb8bbd6..bc9d31b9b3f 100644 --- a/packages/@glimmer-workspace/integration-tests/test/debug-render-tree-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/debug-render-tree-test.ts @@ -14,7 +14,7 @@ import { expect } from '@glimmer/debug-util'; import { DEBUG } from '@glimmer/env'; import { modifierCapabilities, setComponentTemplate, setModifierManager } from '@glimmer/manager'; import { EMPTY_ARGS, templateOnlyComponent, TemplateOnlyComponentManager } from '@glimmer/runtime'; -import { assign } from '@glimmer/util'; +import DebugRenderTreeImpl from '@glimmer/runtime/lib/debug-render-tree'; import { BaseEnv, createTemplate, @@ -906,7 +906,14 @@ class DebugRenderTreeTest extends RenderTest { } suite(DebugRenderTreeTest, DebugRenderTreeDelegate, { - env: assign({}, BaseEnv, { - enableDebugTooling: true, - }), + env: { + ...BaseEnv, + // A fresh DebugRenderTree per environment, mirroring what + // `EmberEnvironmentDelegate` does when debug tooling is enabled. + // (Declared as a getter in the literal so each environment gets its + // own instance; `assign` would eagerly evaluate it.) + get debugRenderTree() { + return new DebugRenderTreeImpl(); + }, + }, }); diff --git a/packages/@glimmer-workspace/integration-tests/test/env-test.ts b/packages/@glimmer-workspace/integration-tests/test/env-test.ts index 674843170d8..b2bded1b912 100644 --- a/packages/@glimmer-workspace/integration-tests/test/env-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/env-test.ts @@ -11,7 +11,6 @@ if (DEBUG) { { onTransactionCommit() {}, isInteractive: true, - enableDebugTooling: false, } ); env.begin(); @@ -28,7 +27,6 @@ QUnit.test('ensure commit cleans up when it can', (assert) => { { onTransactionCommit() {}, isInteractive: true, - enableDebugTooling: false, } ); env.begin(); diff --git a/packages/@glimmer/runtime/lib/environment.ts b/packages/@glimmer/runtime/lib/environment.ts index bafbefe2f82..dd26f3cff5c 100644 --- a/packages/@glimmer/runtime/lib/environment.ts +++ b/packages/@glimmer/runtime/lib/environment.ts @@ -2,6 +2,7 @@ import { DEBUG } from '@glimmer/env'; import type { ClassicResolver, ComponentInstanceWithCreate, + DebugRenderTree, Environment, EnvironmentOptions, GlimmerTreeChanges, @@ -19,7 +20,6 @@ import { ProgramImpl } from '@glimmer/program/lib/program'; import { track } from '@glimmer/validator/lib/tracking'; import { UPDATE_TAG as updateTag } from '@glimmer/validator/lib/validators'; -import DebugRenderTree from './debug-render-tree'; import { DOMChangesImpl, DOMTreeConstruction } from './dom/helper'; import { isArgumentError } from './vm/arguments'; @@ -107,15 +107,15 @@ export class EnvironmentImpl implements Environment { // eslint-disable-next-line @typescript-eslint/no-explicit-any isArgumentCaptureError: ((error: any) => boolean) | undefined; - debugRenderTree: DebugRenderTree | undefined; + debugRenderTree: DebugRenderTree | undefined; constructor( options: EnvironmentOptions, private delegate: EnvironmentDelegate ) { this.isInteractive = delegate.isInteractive; - this.debugRenderTree = this.delegate.enableDebugTooling ? new DebugRenderTree() : undefined; - this.isArgumentCaptureError = this.delegate.enableDebugTooling ? isArgumentError : undefined; + this.debugRenderTree = this.delegate.debugRenderTree; + this.isArgumentCaptureError = this.debugRenderTree ? isArgumentError : undefined; if (options.appendOperations) { this.appendOperations = options.appendOperations; this.updateOperations = options.updateOperations; @@ -192,9 +192,14 @@ export interface EnvironmentDelegate { isInteractive: boolean; /** - * Used to enable debug tooling + * When provided, the environment will perform the extra bookkeeping needed + * to power debug tooling (e.g. Ember Inspector's render tree). + * + * This is provided by the delegate (rather than constructed by the + * environment) so that the `DebugRenderTree` implementation can be + * tree-shaken out of builds that do not use it. */ - enableDebugTooling: boolean; + debugRenderTree?: DebugRenderTree | undefined; /** * Callback to be called when an environment transaction commits