From d20f164fc2a88f3d8901055eefc247f2748a615f Mon Sep 17 00:00:00 2001 From: shleewhite Date: Tue, 23 Jun 2026 15:56:14 -0400 Subject: [PATCH 01/12] feat: website dynamic template actually render gts components --- .../app/components/doc/code-group/index.gts | 46 ++++++++++- website/app/components/dynamic-template.gjs | 80 ++++++++++++------- .../showdown-extensions/content-blocks.js | 5 +- .../code/code-snippets/accordion-size.gts | 2 +- website/lib/markdown/index.js | 2 +- .../lib/markdown/markdown-process-demos.js | 10 ++- 6 files changed, 106 insertions(+), 39 deletions(-) diff --git a/website/app/components/doc/code-group/index.gts b/website/app/components/doc/code-group/index.gts index d5226c921c6..7c06a0b02b9 100644 --- a/website/app/components/doc/code-group/index.gts +++ b/website/app/components/doc/code-group/index.gts @@ -23,6 +23,7 @@ import DynamicTemplate from 'website/components/dynamic-template'; interface DocCodeGroupSignature { Args: { filename?: string; + gtsFilename?: string; hbsSnippet?: string; jsSnippet?: string; gtsSnippet?: string; @@ -121,8 +122,13 @@ export default class DocCodeGroup extends Component { // NOTE: the dynamic template requires a component to render a preview. If there is not a component (ex. only a sass/yaml/bash snippet), we hide the preview by default. get hidePreview() { - // TODO: refactor dynamic template to support gts components: https://hashicorp.atlassian.net/browse/HDS-5833 - return this.args.hbsSnippet === '' || this.args.hidePreview === 'true'; + const hasRenderablePreview = + (this.args.hbsSnippet && this.args.hbsSnippet !== '') || + (this.args.gtsSnippet && + this.args.gtsSnippet !== '' && + this.args.filename); + + return !hasRenderablePreview || this.args.hidePreview === 'true'; } get hbsSnippet() { @@ -150,6 +156,38 @@ export default class DocCodeGroup extends Component { return customSnippet ? unescapeCode(customSnippet) : ''; } + get previewTemplateString() { + // For .gts view, the preview comes from a real component module (via + // `@componentId`), so we intentionally skip passing a runtime template. + if (this.currentView === 'gts' && this.args.gtsSnippet !== '') { + return undefined; + } + + return this.hbsSnippet === '' ? undefined : this.hbsSnippet; + } + + get previewComponentId() { + const { filename, gtsFilename } = this.args; + + if (!filename) { + return undefined; + } + + if (this.currentView === 'gts' && this.args.gtsSnippet !== '') { + if (gtsFilename && gtsFilename !== '') { + // .gts demos can live under a different component file than the .hbs + // classic demo, so prefer an explicit gts filename when provided. + return gtsFilename; + } + + // Fallback: map `foo.classic` docs entries to the colocated modern + // component id (`foo`) used by the .gts demo implementation. + return filename.replace(/\.classic$/, ''); + } + + return filename; + } + get currentSnippet() { if (this.currentView === 'js') { return { snippet: this.jsSnippet, language: 'js' }; @@ -282,8 +320,8 @@ export default class DocCodeGroup extends Component { {{#if (notEq this.hidePreview true)}}
{{/if}} diff --git a/website/app/components/dynamic-template.gjs b/website/app/components/dynamic-template.gjs index 0067c6830c8..f7559373ceb 100644 --- a/website/app/components/dynamic-template.gjs +++ b/website/app/components/dynamic-template.gjs @@ -19,58 +19,78 @@ export default class DynamicTemplate extends Component { let owner = getOwner(this); let templateMap = templateOwnerMap.get(owner); if (templateMap === undefined) { - templateMap = templateOwnerMap.set(owner, new Map()); + templateMap = new Map(); + templateOwnerMap.set(owner, templateMap); } this.templateMap = templateMap; } - get component() { + get resolvedComponent() { let owner = getOwner(this); - let { templateString } = this.args; - if (!templateString) { - return null; + let factory = owner.factoryFor(`component:${this.args.componentId}`); + if (factory?.class && typeof factory.class === 'function') { + return class extends factory.class {}; } - let component = this.templateMap.get(templateString); - if (component === undefined) { - let compiledTemplate; + // if component couldn't be found the old way try importing it directly + let module; + try { + module = importSync(`./${this.args.componentId}.gts`); + } catch { try { - compiledTemplate = compileTemplate(templateString); - } catch (err) { - console.error(err); - console.error(templateString); - compiledTemplate = compileTemplate(``); + module = importSync(`./${this.args.componentId}.js`); + } catch { + // backing class doesn't exist so just ignore the error } + } - component = owner.factoryFor(`component:${this.args.componentId}`); + let defaultExport = module?.default; + if (typeof defaultExport === 'function') { + return defaultExport; + } - if (component) { - component = class extends component.class {}; - } else { - // if component couldn't be found the old way try importing it directly - let module; + return null; + } + + get component() { + let { componentId } = this.args; + let { templateString } = this.args; + let cacheKey = `${componentId || ''}::${templateString || ''}`; + + let component = this.templateMap.get(cacheKey); + if (component === undefined) { + if (templateString) { + // .hbs examples are provided as raw template strings, so we compile + // them at runtime and attach them to a backing class (if one exists). + component = this.resolvedComponent; + + let compiledTemplate; try { - module = importSync(`./docs/${this.args.componentId}.js`); - } catch { - // backing class doesn't exist so just ignore the error + compiledTemplate = compileTemplate(templateString); + } catch (err) { + console.error(err); + console.error(templateString); + compiledTemplate = compileTemplate(``); } - component = module?.default; - } + if (!component) { + component = class extends Component {}; + } - if (!component) { - component = class extends Component {}; + setComponentTemplate(compiledTemplate, component); + } else { + // .gts examples are precompiled modules with their own template + class, + // so we render the component by id instead of compiling a template string. + component = componentId || null; } - setComponentTemplate(compiledTemplate, component); - // eslint-disable-next-line ember/no-side-effects - this.templateMap.set(templateString, component); + this.templateMap.set(cacheKey, component); } return component; } - + } diff --git a/website/app/shared/showdown-extensions/content-blocks.js b/website/app/shared/showdown-extensions/content-blocks.js index 1f3fef3d8e0..7e594c6fb51 100644 --- a/website/app/shared/showdown-extensions/content-blocks.js +++ b/website/app/shared/showdown-extensions/content-blocks.js @@ -53,7 +53,7 @@ export const contentBlocks = function () { // NOTE: this regex must match the output created in markdown-process-demos.js const demoRegex = new RegExp( - /<\?php start="demo-block" filename="(.*?)" hbs="(.*?)" js="(.*?)" gts="(.*?)" compactGts="(.*?)" custom="(.*?)" customLang="(.*?)" hidePreview="(.*?)" expanded="(.*?)" \?>\n?/, + /<\?php start="demo-block" filename="(.*?)"(?: gtsFilename="(.*?)")? hbs="(.*?)" js="(.*?)" gts="(.*?)" compactGts="(.*?)" custom="(.*?)" customLang="(.*?)" hidePreview="(.*?)" expanded="(.*?)" \?>\n?/, 'g', ); @@ -62,6 +62,7 @@ export const contentBlocks = function () { function ( _match, filename, + gtsFilename, hbs, js, gts, @@ -71,7 +72,7 @@ export const contentBlocks = function () { hidePreview, expanded, ) { - return `\n`; + return `\n`; }, ); diff --git a/website/docs/components/accordion/partials/code/code-snippets/accordion-size.gts b/website/docs/components/accordion/partials/code/code-snippets/accordion-size.gts index a2292d60e6f..c13bd0a28fb 100644 --- a/website/docs/components/accordion/partials/code/code-snippets/accordion-size.gts +++ b/website/docs/components/accordion/partials/code/code-snippets/accordion-size.gts @@ -5,7 +5,7 @@ import { HdsAccordion } from '@hashicorp/design-system-components/components'; const LocalComponent: TemplateOnlyComponent =