Support "npm run writers" to display a list of writer-focused tools (#58326)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
/**
|
||||
* This script iterates over all pages and all reusables and looks for
|
||||
* mentions of variables in Liquid syntax. For example,
|
||||
* @purpose Writer tool
|
||||
* @description Look for mentions of variables in Liquid syntax across all pages
|
||||
*
|
||||
* For example,
|
||||
*
|
||||
* ---
|
||||
* title: '{% data variables.product.prodname_mobile %} is cool'
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* @purpose Writer tool
|
||||
* @description Run the Docs content linter, specifying paths and optional rules
|
||||
*/
|
||||
// @ts-nocheck
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// This script auto-populates the `contentType` frontmatter property based on
|
||||
// the directory location of the content file.
|
||||
// Run with:
|
||||
// npm run-script -- add-content-type --help
|
||||
/**
|
||||
* @purpose Writer tool
|
||||
* @description Auto-populate the `contentType` frontmatter property based on the directory location of the content file
|
||||
*/
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* @purpose Writer tool
|
||||
* @description Create a properly formatted Call-to-Action URL with tracking parameters
|
||||
*/
|
||||
import { Command } from 'commander'
|
||||
import readline from 'readline'
|
||||
import chalk from 'chalk'
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* @purpose Writer tool
|
||||
* @description Move or rename a file or a folder and automatically add redirects
|
||||
*/
|
||||
// @ts-nocheck
|
||||
// [start-readme]
|
||||
//
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* @purpose Writer tool
|
||||
* @description Find all content files that use a specific reusable
|
||||
*/
|
||||
// Usage: npm run reusables -- --help
|
||||
// Usage: npm run reusables -- find used accounts/create-account.md
|
||||
// Usage: npm run reusables -- find unused accounts/create-account.md
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
// [start-readme]
|
||||
//
|
||||
// Run this script to update filepaths to match short titles (or titles as a fallback).
|
||||
// Use
|
||||
// npm run-script -- update-filepaths --help
|
||||
//
|
||||
// [end-readme]
|
||||
/**
|
||||
* @purpose Writer tool
|
||||
* @description Update content filenames to match short titles
|
||||
*/
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/**
|
||||
* Development tool that generates a local Table of Contents (TOC) for the GitHub Docs website.
|
||||
* @purpose Writer tool
|
||||
* @description Generate a local table of contents for the GitHub Docs website
|
||||
*
|
||||
* This script creates static HTML files for each documentation version, renders page titles
|
||||
* using Liquid templating, and opens the generated TOC in your browser for easy navigation
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# [start-readme]
|
||||
#
|
||||
# This script is run on a writer's machine to begin developing Early Access content locally.
|
||||
#
|
||||
# [end-readme]
|
||||
# @purpose Writer tool
|
||||
# @description Clone the docs-early-access repo
|
||||
|
||||
set -e
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# [start-readme]
|
||||
#
|
||||
# This script is run on a writer's machine to create an Early Access branch that matches the current docs-internal branch.
|
||||
#
|
||||
# [end-readme]
|
||||
# @purpose Writer tool
|
||||
# @description Create matching branches in docs-early-access and docs-internal
|
||||
|
||||
set -e
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
// [start-readme]
|
||||
//
|
||||
// This script is run on a writer's machine while developing Early Access content locally.
|
||||
// You must pass the script the location of your local copy of
|
||||
// the `github/docs-early-access` git repo as the first argument.
|
||||
//
|
||||
// [end-readme]
|
||||
/**
|
||||
* @purpose Writer tool
|
||||
* @description Create or destroy symlinks to your local docs-early-access checkout
|
||||
*/
|
||||
|
||||
import { rimraf } from 'rimraf'
|
||||
import fs from 'fs'
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// [start-readme]
|
||||
//
|
||||
// This script is run on a writer's machine while developing Early Access content locally. It
|
||||
// updates the data and image paths to either include `early-access` or remove it.
|
||||
//
|
||||
// [end-readme]
|
||||
/**
|
||||
* @purpose Writer tool
|
||||
* @description Add or remove "early-access" from data and image paths
|
||||
*/
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @purpose Writer tool
|
||||
* @description Get data about a top-level docs product and output a CSV
|
||||
*/
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @purpose Writer tool
|
||||
* @description Get a data snapshot of a given Docs URL for the last 30 days or specified period
|
||||
*/
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { Command } from 'commander'
|
||||
|
||||
215
src/workflows/writers-help-metadata.ts
Normal file
215
src/workflows/writers-help-metadata.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
import { readFileSync } from 'fs'
|
||||
import { glob } from 'glob'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __scriptname = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__scriptname)
|
||||
|
||||
const PURPOSE_STRING = '@purpose Writer tool'
|
||||
const DESCRIPTION_STRING = '@description'
|
||||
const DESCRIPTION_REGEX = new RegExp(`${DESCRIPTION_STRING}\\s+(.+)`)
|
||||
|
||||
interface WriterTool {
|
||||
name: string
|
||||
description: string
|
||||
priority?: number // Lower numbers = higher priority
|
||||
}
|
||||
|
||||
interface WriterToolsCollection {
|
||||
[category: string]: WriterTool[]
|
||||
}
|
||||
|
||||
interface ScriptMetadata {
|
||||
isWriterTool?: boolean
|
||||
category?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
// Manual entries for scripts that aren't TypeScript files with metadata
|
||||
const MANUAL_ENTRIES: WriterToolsCollection = {
|
||||
'Validation and formatting': [
|
||||
{ name: 'prettier', description: 'Format markdown, YAML, and other files' },
|
||||
],
|
||||
Development: [
|
||||
{ name: 'dev', description: 'Start local development server' },
|
||||
{ name: 'build', description: 'Build the application' },
|
||||
],
|
||||
}
|
||||
|
||||
async function discoverWriterTools(): Promise<WriterToolsCollection> {
|
||||
const packageJsonPath = path.join(__dirname, '..', '..', 'package.json')
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
|
||||
const tools: WriterToolsCollection = { ...MANUAL_ENTRIES } // Start with manual entries
|
||||
|
||||
// First get all files
|
||||
const allFiles = await glob('src/**/*', {
|
||||
cwd: path.join(__dirname, '..', '..'),
|
||||
absolute: true,
|
||||
ignore: ['**/node_modules/**', '**/tests/**', '**/test/**', '**/.*'],
|
||||
})
|
||||
|
||||
// Then filter for .ts, .js, .sh scripts
|
||||
const scriptFiles = allFiles.filter((file) => {
|
||||
if (file === __scriptname) return false // skip the current file
|
||||
|
||||
const ext = path.extname(file)
|
||||
if (['.ts', '.js', '.sh'].includes(ext)) return true
|
||||
|
||||
// For extensionless files, check if they're executable or have shebang
|
||||
if (ext === '') {
|
||||
try {
|
||||
const content = readFileSync(file, 'utf8')
|
||||
return content.startsWith('#!/bin/bash') || content.startsWith('#!/usr/bin/env bash')
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
for (const filePath of scriptFiles) {
|
||||
try {
|
||||
const relativePath = path.relative(process.cwd(), filePath)
|
||||
const content = readFileSync(filePath, 'utf8')
|
||||
const metadata = extractMetadata(content)
|
||||
|
||||
if (metadata.isWriterTool) {
|
||||
metadata.category = getCategory(relativePath)
|
||||
// Find corresponding npm script
|
||||
const scriptName = findScriptName(packageJson.scripts, relativePath)
|
||||
if (scriptName) {
|
||||
if (!tools[metadata.category]) tools[metadata.category] = []
|
||||
|
||||
// Check if not already added manually
|
||||
const exists = tools[metadata.category].some((tool) => tool.name === scriptName)
|
||||
if (!exists) {
|
||||
tools[metadata.category].push({
|
||||
name: scriptName,
|
||||
description: metadata.description || `${scriptName} tool`,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip files that can't be read
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return tools
|
||||
}
|
||||
|
||||
function extractMetadata(content: string): ScriptMetadata {
|
||||
const metadata: ScriptMetadata = {}
|
||||
const lines = content.split('\n').slice(0, 20) // Only check first 20 lines
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes(PURPOSE_STRING)) {
|
||||
metadata.isWriterTool = true
|
||||
}
|
||||
|
||||
if (line.includes(DESCRIPTION_STRING)) {
|
||||
// Extract description from line like "@description Add content type frontmatter to articles"
|
||||
const match = line.match(DESCRIPTION_REGEX)
|
||||
if (match) {
|
||||
metadata.description = match[1].trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
// Convert the DIR in src/DIR/ to a title-cased category name
|
||||
// E.g. src/secret-scanning becomes Secret Scanning
|
||||
function getCategory(relativePath: string): string {
|
||||
const directory = relativePath.split(path.sep)[1]
|
||||
const category = directory
|
||||
.split('-')
|
||||
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
||||
.join(' ')
|
||||
|
||||
// Clarify this one category
|
||||
return category.replace('Content Render', 'Content Tasks')
|
||||
}
|
||||
|
||||
function findScriptName(scripts: Record<string, string>, relativePath: string): string | null {
|
||||
for (const [scriptName, command] of Object.entries(scripts)) {
|
||||
// Check if the command includes this file path
|
||||
if (command.includes(relativePath)) {
|
||||
return scriptName
|
||||
}
|
||||
// Also check for simplified paths without the src/ prefix
|
||||
const simplifiedPath = relativePath.replace(/^src\//, '')
|
||||
if (command.includes(simplifiedPath)) {
|
||||
return scriptName
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function prioritizeOrder(tools: WriterToolsCollection) {
|
||||
// Define priorities for specific tools
|
||||
const priorities = {
|
||||
'move-content': 1,
|
||||
'cta-builder': 2,
|
||||
'lint-content': 1,
|
||||
docstat: 1,
|
||||
dev: 1,
|
||||
}
|
||||
|
||||
// Assign priorities to discovered tools
|
||||
Object.values(tools)
|
||||
.flat()
|
||||
.forEach((tool) => {
|
||||
if (priorities[tool.name as keyof typeof priorities]) {
|
||||
tool.priority = priorities[tool.name as keyof typeof priorities]
|
||||
}
|
||||
})
|
||||
|
||||
// Sort each category by priority, then alphabetically
|
||||
Object.keys(tools).forEach((category) => {
|
||||
tools[category].sort((a, b) => {
|
||||
// Items with priority come first
|
||||
if (a.priority !== undefined && b.priority === undefined) return -1
|
||||
if (a.priority === undefined && b.priority !== undefined) return 1
|
||||
|
||||
// Both have priority: sort by priority value
|
||||
if (a.priority !== undefined && b.priority !== undefined) {
|
||||
return a.priority - b.priority
|
||||
}
|
||||
|
||||
// Neither has priority: sort alphabetically
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
})
|
||||
|
||||
return tools
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
console.log('For more info, run a command with "-- --help".\n')
|
||||
|
||||
const tools = prioritizeOrder(await discoverWriterTools())
|
||||
|
||||
Object.entries(tools).forEach(([category, scripts]) => {
|
||||
console.log(`${category}:`)
|
||||
scripts.forEach((script) => {
|
||||
const padding = ' '.repeat(Math.max(0, 34 - script.name.length))
|
||||
console.log(` npm run ${script.name}${padding}# ${script.description}`)
|
||||
})
|
||||
console.log('')
|
||||
})
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
try {
|
||||
await main()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user