-
Notifications
You must be signed in to change notification settings - Fork 231
Add script for TOML config schema doc generation #6979
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
393ff45
33fc9f7
80eda46
77b8d03
76f7264
c86df50
48b8737
2a13213
8c897e7
4a52b92
3e1add3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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" | ||
| # 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" | ||
| 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>') | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
in the code just call |
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we need this new file? we can update the existing {
"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": [] | ||
| } |
| 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 |
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tried to run this, but got this error:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changint the package.json to: 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", | ||
|
|
@@ -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": [ | ||
|
|
||
| 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`, | ||
| ) | ||
| } |
There was a problem hiding this comment.
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.