Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 36 additions & 4 deletions bin/docs/build-dev-docs.sh
Original file line number Diff line number Diff line change
@@ -1,17 +1,49 @@
echo "STARTING"
COMPILE_DOCS="npx tsc --project bin/docs/tsconfig.docs.json --moduleResolution node --target esNext && npx generate-docs --overridePath ./bin/docs/typeOverride.json --input ./docs-shopify.dev/commands --output ./docs-shopify.dev/generated && rm -rf docs-shopify.dev/commands/**/*.doc.js docs-shopify.dev/commands/*.doc.js"

# Check if schema docs exist (generated by `pnpm generate-schema-docs`)
HAS_SCHEMA_DOCS=false
if [ -d "docs-shopify.dev/configuration" ] && [ "$(ls -A docs-shopify.dev/configuration/*.doc.ts 2>/dev/null)" ]; then
HAS_SCHEMA_DOCS=true
fi

# Step 1: Compile TypeScript for commands
COMPILE_CMD_TS="npx tsc --project bin/docs/tsconfig.docs.json --moduleResolution node --target esNext"
# Step 2: Compile TypeScript for schema docs (if present)
COMPILE_SCHEMA_TS="npx tsc --project bin/docs/tsconfig.schema-docs.json --moduleResolution node --target esNext"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we put the schema tsconfig in the same file we don't need this extra line.

# Step 3: Run generate-docs with all inputs at once so they end up in a single output file
GENERATE_DOCS_INPUT="./docs-shopify.dev/commands"
CLEANUP="rm -rf docs-shopify.dev/commands/**/*.doc.js docs-shopify.dev/commands/*.doc.js"

COMPILE_STATIC_PAGES="npx tsc docs-shopify.dev/static/*.doc.ts --moduleResolution node --target esNext && npx generate-docs --isLandingPage --input ./docs-shopify.dev/static --output ./docs-shopify.dev/generated && rm -rf docs-shopify.dev/static/*.doc.js"
COMPILE_CATEGORY_PAGES="npx tsc docs-shopify.dev/categories/*.doc.ts --moduleResolution node --target esNext && generate-docs --isCategoryPage --input ./docs-shopify.dev/categories --output ./docs-shopify.dev/generated && rm -rf docs-shopify.dev/categories/*.doc.js"
COMPILE_CATEGORY_PAGES="npx tsc docs-shopify.dev/categories/*.doc.ts --moduleResolution node --target esNext && npx generate-docs --isCategoryPage --input ./docs-shopify.dev/categories --output ./docs-shopify.dev/generated && rm -rf docs-shopify.dev/categories/*.doc.js"

OUTPUT_DIR="./docs-shopify.dev/generated"

if [ "$1" = "isTest" ];
then
COMPILE_DOCS="npx tsc --project bin/docs/tsconfig.docs.json --moduleResolution node --target esNext && npx generate-docs --overridePath ./bin/docs/typeOverride.json --input ./docs-shopify.dev/commands --output ./docs-shopify.dev/static/temp && rm -rf docs-shopify.dev/commands/**/*.doc.js docs-shopify.dev/commands/*.doc.js"
OUTPUT_DIR="./docs-shopify.dev/static/temp"
COMPILE_STATIC_PAGES="npx tsc docs-shopify.dev/static/*.doc.ts --moduleResolution node --target esNext && npx generate-docs --isLandingPage --input ./docs-shopify.dev/static/docs-shopify.dev --output ./docs-shopify.dev/static/temp && rm -rf docs-shopify.dev/static/*.doc.js"
fi

echo $1
echo "RUNNING"
eval $COMPILE_DOCS

# Compile command docs TypeScript
eval $COMPILE_CMD_TS

# Compile schema docs TypeScript if present, and add to input
if [ "$HAS_SCHEMA_DOCS" = true ]; then
eval $COMPILE_SCHEMA_TS
GENERATE_DOCS_INPUT="./docs-shopify.dev/commands ./docs-shopify.dev/configuration"
CLEANUP="$CLEANUP && rm -rf docs-shopify.dev/configuration/**/*.doc.js docs-shopify.dev/configuration/*.doc.js"
fi

# Generate all reference entity docs in a single pass
# Note: $GENERATE_DOCS_INPUT is intentionally unquoted — it may contain two space-separated
# paths that must split into separate arguments for --input.
npx generate-docs --overridePath ./bin/docs/typeOverride.json --input $GENERATE_DOCS_INPUT --output $OUTPUT_DIR
eval $CLEANUP

eval $COMPILE_STATIC_PAGES
eval $COMPILE_CATEGORY_PAGES
echo "DONE"
13 changes: 13 additions & 0 deletions bin/docs/generate-schema-docs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

import {join} from 'node:path'

import {generateSchemaDocs} from '../../packages/app/dist/cli/services/docs/generate-schema-docs.js'

const clientId = process.argv[2]
if (!clientId) {
console.error('Usage: node bin/docs/generate-schema-docs.js <client-id>')
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we will need to pass a client id in order to dump extension data until we have a static way to grab schemas without being app-scoped.

process.exit(1)
}

const basePath = join(process.cwd(), 'docs-shopify.dev/configuration')
await generateSchemaDocs(basePath, clientId)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of having this as a standalone JS file, should we add this to the existing shopify docs generate command?
We can add a flag there to select whether or not we want to generate the schema docs or not.
And that flag can be a CI secret so it is automatically included in CI.

shopify docs generate --schemas_client_id=<client_id>

in the code just call
await generateSchemaDocs(basePath, flags.schemas_client_id)

9 changes: 9 additions & 0 deletions bin/docs/tsconfig.schema-docs.json
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need this new file? we can update the existing tsconfig.docs.json and add a new entry in include:

{
    "compilerOptions": {
        "rootDir": "/",
    },
    "include": [
        "../../docs-shopify.dev/commands/**/*.doc.ts",
        "../../docs-shopify.dev/configuration/**/*.doc.ts"
    ],
    "exclude": []
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"rootDir": "/"
},
"include": [
"../../docs-shopify.dev/configuration/**/*.doc.ts"
],
"exclude": []
}
10 changes: 10 additions & 0 deletions docs-shopify.dev/categories/app-configuration.doc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {CategoryTemplateSchema} from '@shopify/generate-docs'

const data: CategoryTemplateSchema = {
// Name of the category
category: 'app-configuration',
title: 'App configuration (app.toml)',
sections: [],
}

export default data
10 changes: 10 additions & 0 deletions docs-shopify.dev/categories/extension-configuration.doc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {CategoryTemplateSchema} from '@shopify/generate-docs'

const data: CategoryTemplateSchema = {
// Name of the category
category: 'extension-configuration',
title: 'Extension configuration (extension.toml)',
sections: [],
}

export default data
10 changes: 10 additions & 0 deletions docs-shopify.dev/generated/generated_category_pages.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
[
{
"category": "app-configuration",
"title": "App configuration (app.toml)",
"sections": []
},
{
"category": "app",
"title": "Shopify CLI App commands",
"sections": []
},
{
"category": "extension-configuration",
"title": "Extension configuration (extension.toml)",
"sections": []
},
{
"category": "general-commands",
"title": "Shopify CLI General commands",
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"create-app": "nx build create-app && node packages/create-app/bin/dev.js --package-manager npm",
"deploy-experimental": "node bin/deploy-experimental.js",
"graph": "nx graph",
"generate-schema-docs": "node bin/docs/generate-schema-docs.js",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tried to run this, but got this error:

node:internal/modules/esm/resolve:313
  return new ERR_PACKAGE_PATH_NOT_EXPORTED(
         ^

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './v3' is not defined by "exports" in /Users/isaac/src/github.com/Shopify/cli/node_modules/.pnpm/zod-to-json-schema@3.25.1_zod@3.24.4/node_modules/zod/package.json imported from /Users/isaac/src/github.com/Shopify/cli/node_modules/.pnpm/zod-to-json-schema@3.25.1_zod@3.24.4/node_modules/zod-to-json-schema/dist/esm/parsers/array.js

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changint the package.json to:

"zod-to-json-schema": "~3.24.1"

Fixes it (the previous config was forcing a different zod version)

"graphql-codegen:get-graphql-schemas": "bin/get-graphql-schemas.js",
"graphql-codegen": "nx run-many --target=graphql-codegen --all",
"knip": "knip",
Expand Down Expand Up @@ -193,7 +194,9 @@
"packages/app": {
"entry": [
"**/{commands,hooks}/**/*.ts!",
"**/index.ts!"
"**/index.ts!",
"src/cli/services/docs/generate-schema-docs.ts",
"src/cli/services/docs/schema-to-docs.ts"
],
"project": "**/*.{ts,tsx}!",
"ignore": [
Expand Down
3 changes: 2 additions & 1 deletion packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@
"@types/react-dom": "^19.0.0",
"@types/which": "3.0.4",
"@types/ws": "^8.5.13",
"@vitest/coverage-istanbul": "^3.1.4"
"@vitest/coverage-istanbul": "^3.1.4",
"zod-to-json-schema": "^3.24.1"
},
"engines": {
"node": ">=20.10.0"
Expand Down
150 changes: 150 additions & 0 deletions packages/app/src/cli/services/docs/generate-schema-docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import {
extractFieldsFromSpec,
zodSchemaToFields,
extensionSlug,
generateAppConfigDocFile,
generateAppConfigSectionInterface,
generateAppConfigExampleToml,
generateExtensionDocFile,
generateExtensionInterfaceFile,
generateExtensionExampleToml,
} from './schema-to-docs.js'
import {appFromIdentifiers} from '../context.js'
import {fetchSpecifications} from '../generate/fetch-extension-specifications.js'
import {AppSchema} from '../../models/app/app.js'

/* eslint-disable @nx/enforce-module-boundaries -- internal tooling, not lazy-loaded at runtime */
import {mkdir, writeFile} from '@shopify/cli-kit/node/fs'
import {joinPath} from '@shopify/cli-kit/node/path'
import {outputInfo, outputSuccess} from '@shopify/cli-kit/node/output'
import type {AppConfigSection, MergedSpec} from './schema-to-docs.js'
/* eslint-enable @nx/enforce-module-boundaries */

/**
* App config specs to skip in docs — these share a schema with another spec and
* would produce duplicate sections. Their fields are already covered by the other spec.
*/
const SKIP_APP_CONFIG_SPECS = new Set([
// Uses the same WebhooksSchema as 'webhooks'; its fields are covered by the Webhooks section
'privacy_compliance_webhooks',
// Branding fields (name, handle) are added to the Global section instead
'branding',
])

/**
* Generate TOML configuration schema documentation files.
*
* Authenticates via the developer platform APIs, fetches extension specifications,
* and writes doc/interface/example files for app config and extensions.
*
* @param basePath - Absolute path to the output directory (e.g. `<repo>/docs-shopify.dev/configuration`)
* @param clientId - The app client ID to authenticate with
*/
export async function generateSchemaDocs(basePath: string, clientId: string): Promise<void> {
outputInfo('Authenticating and fetching app...')
const app = await appFromIdentifiers({apiKey: clientId})
const {developerPlatformClient} = app

outputInfo('Fetching extension specifications...')
const specs = await fetchSpecifications({
developerPlatformClient,
app: {apiKey: app.apiKey, organizationId: app.organizationId, id: app.id},
})

// Partition: single = app.toml config modules, uuid/dynamic = extension types
const appConfigSpecs: MergedSpec[] = []
const extensionSpecs: MergedSpec[] = []
for (const spec of specs) {
const merged = spec as MergedSpec
if (merged.uidStrategy === 'single') {
if (!SKIP_APP_CONFIG_SPECS.has(merged.identifier)) {
appConfigSpecs.push(merged)
}
} else {
extensionSpecs.push(merged)
}
}

outputInfo(
`Found ${specs.length} specifications (${appConfigSpecs.length} app config, ${extensionSpecs.length} extensions). Generating docs...`,
)

// Ensure output directories exist
await mkdir(basePath)
await mkdir(joinPath(basePath, 'interfaces'))
await mkdir(joinPath(basePath, 'examples'))

// --- App configuration: one consolidated page ---

// Start with root-level fields from AppSchema (client_id, build, extension_directories, etc.)
// Also include name and handle which are root-level app.toml fields contributed by the branding spec.
const globalFields = [
...zodSchemaToFields(AppSchema),
{name: 'name', type: 'string', required: true, description: 'The name of your app.'},
{name: 'handle', type: 'string', required: false, description: 'The URL handle of your app.'},
]
const appSections: AppConfigSection[] = [
{
identifier: 'global',
externalName: 'Global',
fields: globalFields,
},
]
outputInfo(` App config section: global (${globalFields.length} fields)`)

const appConfigFieldPromises = appConfigSpecs.map(async (spec) => {
const fields = await extractFieldsFromSpec(spec)
return {
identifier: spec.identifier,
externalName: spec.externalName,
fields,
}
})
const resolvedAppConfigSections = await Promise.all(appConfigFieldPromises)
for (const section of resolvedAppConfigSections) {
appSections.push(section)
outputInfo(` App config section: ${section.identifier} (${section.fields.length} fields)`)
}

const appDocContent = generateAppConfigDocFile(appSections)
await writeFile(joinPath(basePath, 'app-configuration.doc.ts'), appDocContent)

// Write one interface file per app config section
const interfaceWrites = appSections
.filter((section) => section.fields.length > 0)
.map(async (section) => {
const sectionSlug = section.identifier.replace(/_/g, '-')
const interfaceContent = generateAppConfigSectionInterface(section)
await writeFile(joinPath(basePath, 'interfaces', `${sectionSlug}.interface.ts`), interfaceContent)
})
await Promise.all(interfaceWrites)

// Write combined app.toml example
const appExampleContent = generateAppConfigExampleToml(appSections)
await writeFile(joinPath(basePath, 'examples', 'app-configuration.example.toml'), appExampleContent)

// --- Extensions: one page per extension type ---
const extensionWrites = extensionSpecs.map(async (spec) => {
const fields = await extractFieldsFromSpec(spec)
const slug = extensionSlug(spec)

const docContent = generateExtensionDocFile(spec, fields)
await writeFile(joinPath(basePath, `${slug}.doc.ts`), docContent)

if (fields.length > 0) {
const interfaceContent = generateExtensionInterfaceFile(spec, fields)
await writeFile(joinPath(basePath, 'interfaces', `${slug}.interface.ts`), interfaceContent)
}

const exampleContent = generateExtensionExampleToml(spec, fields)
await writeFile(joinPath(basePath, 'examples', `${slug}.example.toml`), exampleContent)

outputInfo(` Extension: ${slug} (${fields.length} fields)`)
})

await Promise.all(extensionWrites)

outputSuccess(
`Generated documentation: 1 app config page (${appSections.length} sections), ${extensionSpecs.length} extension pages`,
)
}
Loading
Loading