Conversation
| const typeDescriptions = JSON.parse(await readFile(typeDescriptionsPath)) as Record<string, string> | ||
| const description = | ||
| typeDescriptions[interfaceName] ?? command.descriptionWithMarkdown ?? command.description ?? command.summary ?? '' | ||
| const cleanDescription = description.replace(/`/g, '\\`') |
Check failure
Code scanning / CodeQL
Incomplete string escaping or encoding High documentation
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 hours ago
In general, the proper fix is to avoid hand-rolled escaping with ad‑hoc string.replace calls and instead use a well-tested escaping routine that correctly handles all the meta-characters relevant to the target context (here, JavaScript template literals). That means making sure both backticks and backslashes (and optionally ${ sequences) are escaped consistently before embedding arbitrary text into a template literal delimited with backticks.
In this specific file, the problematic logic is on lines 92 and 94 where description and previewDescription are sanitized only by replacing backticks. The best fix that preserves existing functionality is to centralize escaping into a small helper function that escapes backslashes first, then backticks, and reuse it for both cleanDescription and cleanPreview. For example, define a function escapeForTemplateLiteral that does: s.replace(/\\/g, '\\\\').replace(//g, '\'). This ensures that any literal backslash in the source becomes \\ inside the template literal (so it is interpreted as a literal backslash), and backticks are escaped as before. Then replace the direct .replace(//g, '\') calls with calls to this helper. No behavior changes besides improved escaping, and no external dependencies are required.
Concretely:
- In
packages/cli/src/cli/commands/docs/generate.ts, above the use ofdescription.replace(...), add a small local helper function, e.g.function escapeForTemplateLiteral(input: string): string { ... }. - Change line 92 to:
const cleanDescription = escapeForTemplateLiteral(description) - Change line 94 to:
const cleanPreview = escapeForTemplateLiteral(previewDescription).replace(/https:\/\/shopify\.dev/g, '')
This keeps all existing logic, including strippinghttps://shopify.devfrom the preview, while correctly handling backslashes and backticks for the generated template literals.
| @@ -73,6 +73,10 @@ | ||
| return {commandName, fileName, interfaceName, hasTopic, topic, hasFlags} | ||
| } | ||
|
|
||
| function escapeForTemplateLiteral(input: string): string { | ||
| return input.replace(/\\/g, '\\\\').replace(/`/g, '\\`') | ||
| } | ||
|
|
||
| // Generates the documentation for a command and writes it to a file (also a file with an example usage of the command) | ||
| export async function writeCommandDocumentation( | ||
| command: CommandWithMarkdown, | ||
| @@ -89,9 +93,9 @@ | ||
| const typeDescriptions = JSON.parse(await readFile(typeDescriptionsPath)) as Record<string, string> | ||
| const description = | ||
| typeDescriptions[interfaceName] ?? command.descriptionWithMarkdown ?? command.description ?? command.summary ?? '' | ||
| const cleanDescription = description.replace(/`/g, '\\`') | ||
| const cleanDescription = escapeForTemplateLiteral(description) | ||
| const previewDescription = command.summary ?? description ?? '' | ||
| const cleanPreview = previewDescription.replace(/`/g, '\\`').replace(/https:\/\/shopify\.dev/g, '') | ||
| const cleanPreview = escapeForTemplateLiteral(previewDescription).replace(/https:\/\/shopify\.dev/g, '') | ||
|
|
||
| const category = hasTopic && !generalTopics.includes(topic!) ? topic : 'general commands' | ||
|
|
🎟️ Part of https://github.com/shop/issues-learn/issues/1466
🎩 Tophatting related PR: https://github.com/shop/world/pull/577553
Source of truth: docs-shopify.dev/type-descriptions.json — a JSON file mapping each interface name (e.g. appbuild) to
its canonical description text. This is a hand-maintained file extracted from the v1 docs.
What changed vs the original generate.ts:
- Before: description came from command.descriptionWithMarkdown ?? command.description ?? command.summary (oclif
command object)
- Now: reads type-descriptions.json first (line 88-89), uses that as primary source, falls back to oclif command if
the type isn't in the JSON (line 90-91)
- This ensures the v1 .doc.ts description matches the v2 JSDoc description
- Before: no JSDoc, no description, no @publicdocs tag
- Now: reads the same type-descriptions.json (line 162-163), gets the description for this interface name (line 164),
formats it as a JSDoc comment (lines 165-168), and adds @publicdocs (line 173)
- This is what generate-docs v2 scans to produce generated_docs_data_v2.json
Data flow:
type-descriptions.json
│
├──> writeCommandDocumentation() ──> .doc.ts files ──> generate-docs v1 ──> generated_docs_data.json
│
└──> writeCommandFlagInterface() ──> .interface.ts files (with @publicdocs JSDoc)
│
└──> generate-docs v2 ──> generated_docs_data_v2.json
Both pipelines read descriptions from the same JSON file, so they produce matching output.