diff --git a/packages/@glimmer-workspace/integration-tests/lib/compile.ts b/packages/@glimmer-workspace/integration-tests/lib/compile.ts index 169dba10d2..012278e876 100644 --- a/packages/@glimmer-workspace/integration-tests/lib/compile.ts +++ b/packages/@glimmer-workspace/integration-tests/lib/compile.ts @@ -4,7 +4,7 @@ import type { Template, TemplateFactory, } from '@glimmer/interfaces'; -import type { PrecompileOptions } from '@glimmer/syntax'; +import type { PrecompileOptions, PrecompileOptionsWithLexicalScope } from '@glimmer/syntax'; import { precompileJSON } from '@glimmer/compiler'; import { templateFactory } from '@glimmer/opcode-compiler'; @@ -19,13 +19,17 @@ let templateId = 0; export function createTemplate( templateSource: Nullable, - options: PrecompileOptions = {}, + options: PrecompileOptions | PrecompileOptionsWithLexicalScope = {}, scopeValues: Record = {} ): TemplateFactory { options.locals = options.locals ?? Object.keys(scopeValues ?? {}); let [block, usedLocals] = precompileJSON(templateSource, options); let reifiedScopeValues = usedLocals.map((key) => scopeValues[key]); + if ('emit' in options && options.emit?.debugSymbols) { + block.push(usedLocals); + } + let templateBlock: SerializedTemplateWithLazyBlock = { id: String(templateId++), block: JSON.stringify(block), diff --git a/packages/@glimmer-workspace/integration-tests/lib/test-helpers/define.ts b/packages/@glimmer-workspace/integration-tests/lib/test-helpers/define.ts index 74e7ac7ee9..e20d3940e0 100644 --- a/packages/@glimmer-workspace/integration-tests/lib/test-helpers/define.ts +++ b/packages/@glimmer-workspace/integration-tests/lib/test-helpers/define.ts @@ -5,6 +5,7 @@ import type { ModifierManager, Owner, } from '@glimmer/interfaces'; +import type { PrecompileOptionsWithLexicalScope } from '@glimmer/syntax'; import { registerDestructor } from '@glimmer/destroyable'; import { helperCapabilities, @@ -99,6 +100,8 @@ export interface DefineComponentOptions { // additional strict-mode keywords keywords?: string[]; + + emit?: PrecompileOptionsWithLexicalScope['emit']; } export function defineComponent( @@ -116,7 +119,7 @@ export function defineComponent( let keywords = options.keywords ?? []; let definition = options.definition ?? templateOnlyComponent(); - let templateFactory = createTemplate(templateSource, { strictMode, keywords }, scopeValues ?? {}); + let templateFactory = createTemplate(templateSource, { strictMode, keywords, emit: options.emit }, scopeValues ?? {}); setComponentTemplate(templateFactory, definition); return definition; } diff --git a/packages/@glimmer/compiler/lib/compiler.ts b/packages/@glimmer/compiler/lib/compiler.ts index d1c831d1ab..28238f447b 100644 --- a/packages/@glimmer/compiler/lib/compiler.ts +++ b/packages/@glimmer/compiler/lib/compiler.ts @@ -119,6 +119,10 @@ export function precompile( ): TemplateJavascript { const [block, usedLocals] = precompileJSON(source, options); + if ('emit' in options && options.emit?.debugSymbols && usedLocals.length > 0) { + block.push(usedLocals); + } + const moduleName = options.meta?.moduleName; const idFn = options.id || defaultId; const blockJSON = JSON.stringify(block); diff --git a/packages/@glimmer/interfaces/lib/compile/wire-format/api.d.ts b/packages/@glimmer/interfaces/lib/compile/wire-format/api.d.ts index 67d420bb1b..eecb62a15b 100644 --- a/packages/@glimmer/interfaces/lib/compile/wire-format/api.d.ts +++ b/packages/@glimmer/interfaces/lib/compile/wire-format/api.d.ts @@ -49,8 +49,8 @@ import type { YieldOpcode, } from './opcodes.js'; -export * from './opcodes.js'; -export * from './resolution.js'; +export type * from './opcodes.js'; +export type * from './resolution.js'; export type TupleSyntax = Statement | TupleExpression; @@ -66,7 +66,7 @@ export type ExpressionSexpOpcodeMap = { [TSexpOpcode in TupleExpression[0]]: Extract; }; -export interface SexpOpcodeMap extends ExpressionSexpOpcodeMap, StatementSexpOpcodeMap {} +export interface SexpOpcodeMap extends ExpressionSexpOpcodeMap, StatementSexpOpcodeMap { } export type SexpOpcode = keyof SexpOpcodeMap; export namespace Core { @@ -365,13 +365,14 @@ export type SerializedInlineBlock = [statements: Statements.Statement[], paramet */ export type SerializedTemplateBlock = [ // statements - Statements.Statement[], + statements: Statements.Statement[], // symbols - string[], + symbols: string[], // hasDebug - boolean, + hasDebug: boolean, // upvars - string[], + upvars: string[], + lexicalSymbols?: string[] ]; /** diff --git a/packages/@glimmer/interfaces/lib/components.d.ts b/packages/@glimmer/interfaces/lib/components.d.ts index 9838e1e798..c13971ffa3 100644 --- a/packages/@glimmer/interfaces/lib/components.d.ts +++ b/packages/@glimmer/interfaces/lib/components.d.ts @@ -24,6 +24,7 @@ export interface ComponentDefinition< manager: M; capabilities: CapabilityMask; compilable: CompilableProgram | null; + debugName?: string | undefined; } export interface ComponentInstance< diff --git a/packages/@glimmer/interfaces/lib/program.d.ts b/packages/@glimmer/interfaces/lib/program.d.ts index 8af484bf5b..3a516982ac 100644 --- a/packages/@glimmer/interfaces/lib/program.d.ts +++ b/packages/@glimmer/interfaces/lib/program.d.ts @@ -118,12 +118,14 @@ export interface ResolutionTimeConstants { component( definitionState: ComponentDefinitionState, owner: object, - isOptional?: false + isOptional?: false, + debugName?: string ): ComponentDefinition; component( definitionState: ComponentDefinitionState, owner: object, - isOptional?: boolean + isOptional?: boolean, + debugName?: string ): ComponentDefinition | null; resolvedComponent( diff --git a/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/resolution.ts b/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/resolution.ts index 92a6580cd9..18f491bafc 100644 --- a/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/resolution.ts +++ b/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/resolution.ts @@ -87,14 +87,13 @@ export function resolveComponent( assert(!meta.isStrictMode, 'Strict mode errors should already be handled at compile time'); throw new Error( - `Attempted to resolve a component in a strict mode template, but that value was not in scope: ${ - meta.upvars![expr[1]] ?? '{unknown variable}' + `Attempted to resolve a component in a strict mode template, but that value was not in scope: ${meta.upvars![expr[1]] ?? '{unknown variable}' }` ); } if (type === SexpOpcodes.GetLexicalSymbol) { - let { scopeValues, owner } = meta; + let { scopeValues, owner, debugSymbols } = meta; let definition = expect(scopeValues, 'BUG: scopeValues must exist if template symbol is used')[ expr[1] ]; @@ -102,7 +101,9 @@ export function resolveComponent( then( constants.component( definition as object, - expect(owner, 'BUG: expected owner when resolving component definition') + expect(owner, 'BUG: expected owner when resolving component definition'), + false, + debugSymbols?.at(expr[1]) ) ); } else { @@ -236,7 +237,7 @@ export function resolveComponentOrHelper( let type = expr[0]; if (type === SexpOpcodes.GetLexicalSymbol) { - let { scopeValues, owner } = meta; + let { scopeValues, owner, debugSymbols } = meta; let definition = expect(scopeValues, 'BUG: scopeValues must exist if template symbol is used')[ expr[1] ]; @@ -244,7 +245,8 @@ export function resolveComponentOrHelper( let component = constants.component( definition as object, expect(owner, 'BUG: expected owner when resolving component definition'), - true + true, + debugSymbols?.at(expr[1]) ); if (component !== null) { @@ -316,7 +318,7 @@ export function resolveOptionalComponentOrHelper( let type = expr[0]; if (type === SexpOpcodes.GetLexicalSymbol) { - let { scopeValues, owner } = meta; + let { scopeValues, owner, debugSymbols } = meta; let definition = expect(scopeValues, 'BUG: scopeValues must exist if template symbol is used')[ expr[1] ]; @@ -333,7 +335,8 @@ export function resolveOptionalComponentOrHelper( let component = constants.component( definition, expect(owner, 'BUG: expected owner when resolving component definition'), - true + true, + debugSymbols?.at(expr[1]) ); if (component !== null) { @@ -390,8 +393,7 @@ function lookupBuiltInHelper( // Keyword helper did not exist, which means that we're attempting to use a // value of some kind that is not in scope throw new Error( - `Attempted to resolve a ${type} in a strict mode template, but that value was not in scope: ${ - meta.upvars![expr[1]] ?? '{unknown variable}' + `Attempted to resolve a ${type} in a strict mode template, but that value was not in scope: ${meta.upvars![expr[1]] ?? '{unknown variable}' }` ); } diff --git a/packages/@glimmer/program/lib/constants.ts b/packages/@glimmer/program/lib/constants.ts index bc071aa098..e175da068a 100644 --- a/packages/@glimmer/program/lib/constants.ts +++ b/packages/@glimmer/program/lib/constants.ts @@ -201,7 +201,8 @@ export class ConstantsImpl component( definitionState: ComponentDefinitionState, owner: object, - isOptional?: true + isOptional?: true, + debugName?: string ): ComponentDefinition | null { let definition = this.componentDefinitionCache.get(definitionState); @@ -256,7 +257,12 @@ export class ConstantsImpl this.componentDefinitionCount++; } + if (definition && debugName !== undefined) { + return { ...definition, debugName }; + } else { + return definition; + } } resolvedComponent( diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts index 655c4e52f6..64e679ca87 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts @@ -60,6 +60,7 @@ import { ConcreteBounds } from '../../bounds'; import { hasCustomDebugRenderTreeLifecycle } from '../../component/interfaces'; import { resolveComponent } from '../../component/resolve'; import { isCurriedType, isCurriedValue, resolveCurriedValue } from '../../curried-value'; +import { getDebugName } from '../../debug-render-tree'; import { APPEND_OPCODES } from '../../opcodes'; import createClassListRef from '../../references/class-list'; import { ARGS, CONSTANTS } from '../../symbols'; @@ -417,7 +418,7 @@ APPEND_OPCODES.add(Op.BeginComponentTransaction, (vm, { op1: _state }) => { if (import.meta.env.DEV) { let { definition, manager } = check(vm.fetchValue(_state), CheckComponentInstance); - name = definition.resolvedName ?? manager.getDebugName(definition.state); + name = getDebugName(definition, manager); } vm.beginCacheGroup(name); @@ -674,7 +675,7 @@ APPEND_OPCODES.add(Op.GetComponentSelf, (vm, { op1: _state, op2: _names }) => { vm.updateWith(new DebugRenderTreeUpdateOpcode(bucket)); }); } else { - let name = definition.resolvedName ?? manager.getDebugName(definition.state); + let name = getDebugName(definition, manager); vm.env.debugRenderTree.create(instance, { type: 'component', diff --git a/packages/@glimmer/runtime/lib/debug-render-tree.ts b/packages/@glimmer/runtime/lib/debug-render-tree.ts index 8d110e8284..abea03d38b 100644 --- a/packages/@glimmer/runtime/lib/debug-render-tree.ts +++ b/packages/@glimmer/runtime/lib/debug-render-tree.ts @@ -1,6 +1,7 @@ import type { Bounds, CapturedRenderNode, + ComponentDefinition, DebugRenderTree, Nullable, RenderNode, @@ -37,7 +38,7 @@ export class Ref { this.value = null; } - toString(): String { + toString(): string { let label = `Ref ${this.id}`; if (this.value === null) { @@ -196,3 +197,7 @@ export default class DebugRenderTreeImpl return { parentElement, firstNode, lastNode }; } } + +export function getDebugName(definition: ComponentDefinition, manager = definition.manager): string { + return definition.resolvedName ?? definition.debugName ?? manager.getDebugName(definition.state); +} diff --git a/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts b/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts index 54b9bb84cb..0a69273f61 100644 --- a/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts +++ b/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts @@ -682,6 +682,14 @@ export interface PrecompileOptions extends PreprocessOptions { export interface PrecompileOptionsWithLexicalScope extends PrecompileOptions { lexicalScope: (variable: string) => boolean; + + /** + * If `emit.debugSymbols` is set to `true`, the name of lexical local variables + * will be included in the wire format. + */ + emit?: { + debugSymbols?: boolean; + }, } export interface PreprocessOptions {