Skip to content
Merged
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
46 changes: 11 additions & 35 deletions .github/workflows/generate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ on:
- main
paths:
- '.github/workflows/generate.yml'
- 'openapi.json'
- 'scripts/generate-sdk.mjs'
- 'scripts/generate-types.mjs'
- 'scripts/prettify-base-json.mjs'
- 'scripts/generate-strict-types.mjs'
schedule:
# At 07:23 on every day-of-week from Monday through Friday.
- cron: '23 7 * * 1-5'
Expand Down Expand Up @@ -46,34 +46,20 @@ jobs:

- uses: SocketDev/socket-registry/.github/actions/setup-and-install@4709a2443e5a036bb0cd94e5d1559f138f05994c # main

- name: Fetch latest OpenAPI definition
id: fetch
- name: Generate SDK
run: pnpm run generate-sdk

- name: Check for changes
id: check
run: |
echo "Fetching latest OpenAPI definition..."
curl -sSL https://api.socket.dev/v0/openapi -o openapi-new.json

# Check if file changed
if [ -f openapi.json ]; then
if diff -q openapi.json openapi-new.json > /dev/null; then
echo "No changes detected"
echo "changed=false" >> $GITHUB_OUTPUT
else
echo "Changes detected"
echo "changed=true" >> $GITHUB_OUTPUT
mv openapi-new.json openapi.json
fi
if [ -n "$(git status --porcelain)" ]; then
echo "has_changes=true" >> $GITHUB_OUTPUT
else
echo "OpenAPI file doesn't exist, creating new one"
echo "changed=true" >> $GITHUB_OUTPUT
mv openapi-new.json openapi.json
echo "has_changes=false" >> $GITHUB_OUTPUT
fi

- name: Run post-sync script
if: steps.fetch.outputs.changed == 'true' || github.event.inputs.force == 'true'
run: pnpm run generate-sdk

- name: Configure git
if: steps.fetch.outputs.changed == 'true' || github.event.inputs.force == 'true'
if: steps.check.outputs.has_changes == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
Expand All @@ -82,16 +68,6 @@ jobs:
# Configure git to use the GitHub token for authentication
git config --global url."https://x-access-token:${GH_TOKEN}@github.com/".insteadOf "https://github.com/"

- name: Check for changes
id: check
if: steps.fetch.outputs.changed == 'true' || github.event.inputs.force == 'true'
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "has_changes=true" >> $GITHUB_OUTPUT
else
echo "has_changes=false" >> $GITHUB_OUTPUT
fi

- name: Commit and push changes
if: steps.check.outputs.has_changes == 'true'
run: |
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@
"@types/node": "24.9.2",
"@typescript/native-preview": "7.0.0-dev.20250926.1",
"@vitest/coverage-v8": "4.0.3",
"@sveltejs/acorn-typescript": "1.0.8",
"acorn": "8.15.0",
"del": "8.0.1",
"dev-null-cli": "2.0.0",
"esbuild": "0.25.11",
Expand Down
15 changes: 15 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

168 changes: 74 additions & 94 deletions scripts/generate-sdk.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,82 +2,98 @@
/**
* @fileoverview SDK generation script.
* Orchestrates the complete SDK generation process:
* 1. Prettifies the OpenAPI JSON
* 1. Fetches and formats OpenAPI JSON
* 2. Generates TypeScript types from OpenAPI
* 3. Formats and lints the generated code
* 3. Generates strict types from OpenAPI
*
* Usage:
* node scripts/generate-sdk.mjs
*/

import { spawn } from 'node:child_process'
import { readFileSync, writeFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { promises as fs } from 'node:fs'
import path from 'node:path'

import { parse } from '@babel/parser'
import { default as traverse } from '@babel/traverse'
import * as t from '@babel/types'
import MagicString from 'magic-string'

import { httpGetJson } from '@socketsecurity/lib/http-request'
import { getDefaultLogger } from '@socketsecurity/lib/logger'
import { spawn } from '@socketsecurity/lib/spawn'

import { getRootPath } from './utils/path-helpers.mjs'
import { runCommand } from './utils/run-command.mjs'

const OPENAPI_URL = 'https://api.socket.dev/v0/openapi'

const rootPath = getRootPath(import.meta.url)
const typesPath = resolve(rootPath, 'types/api.d.ts')
const openApiPath = path.resolve(rootPath, 'openapi.json')
const typesPath = path.resolve(rootPath, 'types/api.d.ts')

// Initialize logger
const logger = getDefaultLogger()

async function generateTypes() {
return new Promise((resolve, reject) => {
const child = spawn('node', ['scripts/generate-types.mjs'], {
cwd: rootPath,
stdio: ['inherit', 'pipe', 'inherit'],
})

let output = ''

child.stdout.on('data', data => {
output += data.toString()
})

child.on('exit', code => {
if (code !== 0) {
reject(new Error(`Type generation failed with exit code ${code}`))
return
}
async function fetchOpenApi() {
const data = await httpGetJson(OPENAPI_URL)
await fs.writeFile(openApiPath, JSON.stringify(data, null, 2), 'utf8')
logger.log(`Downloaded from ${OPENAPI_URL}`)
}

try {
writeFileSync(typesPath, output, 'utf8')
// Fix array syntax after writing to disk
fixArraySyntax(typesPath)
// Add SDK v3 method name aliases
addSdkMethodAliases(typesPath)
resolve()
} catch (error) {
reject(error)
}
})
async function generateStrictTypes() {
await spawn('node', ['scripts/generate-strict-types.mjs'], {
cwd: rootPath,
stdio: 'inherit',
})
const exitCode = await runCommand('pnpm', [
'exec',
'biome',
'format',
'--log-level=none',
'--fix',
'src/types-strict.ts',
])
if (exitCode !== 0) {
throw new Error(`Formatting strict types failed with exit code ${exitCode}`)
}
}

child.on('error', reject)
async function generateTypes() {
await spawn('node', ['scripts/generate-types.mjs'], {
cwd: rootPath,
stdio: 'inherit',
})
// Fix array syntax after writing to disk
await fixArraySyntax(typesPath)
// Add SDK v3 method name aliases
await addSdkMethodAliases(typesPath)
// Format generated types
const exitCode = await runCommand('pnpm', [
'exec',
'biome',
'format',
'--log-level=none',
'--fix',
'types/api.d.ts',
])
if (exitCode !== 0) {
throw new Error(`Formatting types failed with exit code ${exitCode}`)
}
}

/**
* Adds SDK v3 method name aliases to the operations interface.
* These aliases map the new SDK method names to their underlying OpenAPI operation names.
* @param {string} filePath - The path to the TypeScript file to update
*/
function addSdkMethodAliases(filePath) {
const content = readFileSync(filePath, 'utf8')
async function addSdkMethodAliases(filePath) {
const content = await fs.readFile(filePath, 'utf8')

// Find the closing brace of the operations interface
const operationsInterfaceEnd = content.lastIndexOf('\n}')

if (operationsInterfaceEnd === -1) {
logger.error(' Could not find operations interface closing brace')
logger.error('Could not find operations interface closing brace')
return
}

Expand All @@ -101,8 +117,8 @@ function addSdkMethodAliases(filePath) {
content.slice(0, operationsInterfaceEnd) +
aliases +
content.slice(operationsInterfaceEnd)
writeFileSync(filePath, updated, 'utf8')
logger.log(' Added SDK v3 method name aliases')
await fs.writeFile(filePath, updated, 'utf8')
logger.log('Added SDK v3 method name aliases')
}

/**
Expand All @@ -111,8 +127,8 @@ function addSdkMethodAliases(filePath) {
* Complex types use Array<T> syntax.
* @param {string} filePath - The path to the TypeScript file to fix
*/
function fixArraySyntax(filePath) {
const content = readFileSync(filePath, 'utf8')
async function fixArraySyntax(filePath) {
const content = await fs.readFile(filePath, 'utf8')
const magicString = new MagicString(content)

// Parse the TypeScript file
Expand Down Expand Up @@ -186,12 +202,8 @@ function fixArraySyntax(filePath) {
},
})

logger.log(
` Found ${transformCount + skipCount} complex arrays to transform`,
)
logger.log(
` Transformed ${transformCount}, skipped ${skipCount} (overlaps)`,
)
logger.log(`Found ${transformCount + skipCount} complex arrays to transform`)
logger.log(`Transformed ${transformCount}, skipped ${skipCount} (overlaps)`)

if (transformCount > 0) {
const transformed = magicString.toString()
Expand All @@ -200,65 +212,33 @@ function fixArraySyntax(filePath) {
const objectArrayCount = (transformed.match(/\}\[\]/g) || []).length
const arrayGenericCount = (transformed.match(/Array</g) || []).length
logger.log(
` Final check: ${objectArrayCount} object arrays with }[], ${arrayGenericCount} Array< generics`,
`Final check: ${objectArrayCount} object arrays with }[], ${arrayGenericCount} Array< generics`,
)

writeFileSync(filePath, transformed, 'utf8')
await fs.writeFile(filePath, transformed, 'utf8')
}
}

async function main() {
try {
logger.log('Generating SDK from OpenAPI...')

// Step 1: Prettify OpenAPI JSON
logger.log(' 1. Prettifying OpenAPI JSON...')
let exitCode = await runCommand('node', ['scripts/prettify-base-json.mjs'])
if (exitCode !== 0) {
process.exitCode = exitCode
return
}
logger.group('Generating SDK from OpenAPI…')

// Step 1: Fetch and format OpenAPI JSON
logger.log('1. Fetching OpenAPI definition…')
await fetchOpenApi()

// Step 2: Generate types
logger.log(' 2. Generating TypeScript types...')
logger.log('2. Generating TypeScript types')
await generateTypes()

// Step 3: Format generated files
logger.log(' 3. Formatting generated files...')
exitCode = await runCommand('pnpm', [
'exec',
'biome',
'format',
'--log-level=none',
'--fix',
'openapi.json',
'types/api.d.ts',
])
if (exitCode !== 0) {
process.exitCode = exitCode
return
}

// Step 4: Run ESLint auto-fix to handle any remaining array syntax issues
logger.log(' 4. Running ESLint auto-fix on types/api.d.ts...')
exitCode = await runCommand('pnpm', [
'exec',
'eslint',
'--config',
'.config/eslint.config.mjs',
'--fix',
'types/api.d.ts',
])
// ESLint returns 0 if successful, 1 if there were fixable issues that were fixed
// Only fail if exit code is 2 (unfixable errors)
if (exitCode === 2) {
logger.error(' ESLint found unfixable errors')
process.exitCode = exitCode
return
}
// Step 3: Generate strict types
logger.log('3. Generating strict types…')
await generateStrictTypes()

logger.groupEnd()
logger.log('SDK generation complete')
} catch (error) {
logger.groupEnd()
logger.error('SDK generation failed:', error.message)
process.exitCode = 1
}
Expand Down
Loading