From 6f2760ded763e6be1dd2d4bccaa90826af2b2391 Mon Sep 17 00:00:00 2001 From: Sean <22557931+sean0407@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:42:17 +0800 Subject: [PATCH] fix(features): preserve literal dollars in generated docs --- .../generateDocsCommandImpl.ts | 20 +++++------ .../featuresCLICommands.test.ts | 34 ++++++++++++++++++- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/src/spec-node/collectionCommonUtils/generateDocsCommandImpl.ts b/src/spec-node/collectionCommonUtils/generateDocsCommandImpl.ts index 0a866f818..62d256d03 100644 --- a/src/spec-node/collectionCommonUtils/generateDocsCommandImpl.ts +++ b/src/spec-node/collectionCommonUtils/generateDocsCommandImpl.ts @@ -169,17 +169,17 @@ async function _generateDocumentation( let newReadme = readmeTemplate // Templates & Features - .replace('#{Id}', parsedJson.id) - .replace('#{Name}', parsedJson.name ? `${parsedJson.name} (${parsedJson.id})` : `${parsedJson.id}`) - .replace('#{Description}', parsedJson.description ?? '') - .replace('#{OptionsTable}', generateOptionsMarkdown()) - .replace('#{Notes}', generateNotesMarkdown()) - .replace('#{RepoUrl}', urlToConfig) + .replace('#{Id}', () => parsedJson.id) + .replace('#{Name}', () => parsedJson.name ? `${parsedJson.name} (${parsedJson.id})` : `${parsedJson.id}`) + .replace('#{Description}', () => parsedJson.description ?? '') + .replace('#{OptionsTable}', () => generateOptionsMarkdown()) + .replace('#{Notes}', () => generateNotesMarkdown()) + .replace('#{RepoUrl}', () => urlToConfig) // Features Only - .replace('#{Registry}', ociRegistry) - .replace('#{Namespace}', namespace) - .replace('#{Version}', version) - .replace('#{Customizations}', extensions); + .replace('#{Registry}', () => ociRegistry) + .replace('#{Namespace}', () => namespace) + .replace('#{Version}', () => version) + .replace('#{Customizations}', () => extensions); if (header) { newReadme = header + newReadme; diff --git a/src/test/container-features/featuresCLICommands.test.ts b/src/test/container-features/featuresCLICommands.test.ts index 74bc375c8..9aa75f2d3 100644 --- a/src/test/container-features/featuresCLICommands.test.ts +++ b/src/test/container-features/featuresCLICommands.test.ts @@ -1,6 +1,7 @@ import { assert } from 'chai'; import path from 'path'; -import { existsSync } from 'fs'; +import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs'; +import os from 'os'; import { createPlainLog, LogLevel, makeLog } from '../../spec-utils/log'; import { isLocalFile, readLocalFile } from '../../spec-utils/pfs'; import { createTestCommonParams, ExecResult, shellExec } from '../testUtils'; @@ -814,4 +815,35 @@ describe('tests generateFeaturesDocumentation()', async function () { const invalidDocsExists = await isLocalFile(`${projectFolder}/not-a-feature/README.md`); assert.isFalse(invalidDocsExists); }); + + it('preserves dollar sequences in generated feature documentation', async function () { + const basePath = mkdtempSync(path.join(os.tmpdir(), 'devcontainer-feature-docs-')); + const featurePath = path.join(basePath, 'dollar-content'); + mkdirSync(featurePath); + writeFileSync(path.join(featurePath, 'devcontainer-feature.json'), JSON.stringify({ + id: 'dollar-content', + version: '1.0.0', + name: 'Price $&', + description: 'Keep $\' literally', + options: { + example: { + type: 'string', + description: 'Keep $\' and $$ literally', + }, + }, + })); + writeFileSync(path.join(featurePath, 'NOTES.md'), 'Keep $` and $$ literally.'); + + try { + await generateFeaturesDocumentation(basePath, 'ghcr.io', 'devcontainers/cli', 'devcontainers', 'cli', output); + const readme = readFileSync(path.join(featurePath, 'README.md'), 'utf8'); + assert.include(readme, '# Price $& (dollar-content)'); + assert.include(readme, 'Keep $\' literally'); + assert.include(readme, 'Keep $\' and $$ literally'); + assert.include(readme, 'Keep $` and $$ literally.'); + assert.strictEqual((readme.match(/## Example Usage/g) ?? []).length, 1); + } finally { + rmSync(basePath, { recursive: true, force: true }); + } + }); });