@palantir/pack.sdkgen
v0.2.0
Published
Generic Typescript SDK generator CLI
Readme
@palantir/pack.sdkgen
A flexible CLI tool for generating TypeScript/JavaScript SDKs from schema definitions using customizable templates. The intention is for this to remain generic and not pack-specific, the pack-specific parts are up to the template scripts that are used!
Table of Contents
Overview
@palantir/pack.sdkgen is a scaffolding tool that generates SDK packages from schema files using pluggable templates. Similar to create-react-app, it allows you to:
- Transform schema definitions into fully-configured SDK projects
- Customize generation through configuration and hooks
- Apply template substitutions and run initialization scripts
Usage
Basic Usage
# Create a new SDK using the default template
npx @palantir/pack.sdkgen create my-sdk --schema ./my-schema.json
# Use a specific template
npx @palantir/pack.sdkgen create my-sdk \
--template @myorg/custom-template \
--schema ./my-schema.json
# Use a local template
npx @palantir/pack.sdkgen create my-sdk \
--template ./path/to/template \
--schema ./my-schema.yaml
# Skip dependency installation
npx @palantir/pack.sdkgen create my-sdk \
--schema ./my-schema.json \
--skip-installAvailable Commands
Usage: sdkgen [options] [command]
Generate TypeScript/JavaScript SDKs from schema definitions
Options:
-V, --version output the version number
-h, --help display help for command
Commands:
create [options] <project-name> Create a new SDK project
help [command] display help for commandCreate Command Options
Usage: sdkgen create [options] <project-name>
Create a new SDK project
Options:
-t, --template <template> template to use (default: "default")
-s, --schema <path> path to schema file
--skip-install skip dependency installation
--verbose enable verbose logging
--dry-run preview without writing files
--non-interactive run in non-interactive mode using defaults
--config <path> path to JSON config file with template configuration
--overwrite overwrite existing directory
-h, --help display help for commandCreating Templates
Templates are npm packages or local directories with a specific structure that defines how to generate SDK code.
Template Structure
my-sdk-template/
├── package.json # Template package metadata
├── template.config.js # Template configuration
├── template/ # File templates
│ ├── package.json.ejs
│ ├── src/
│ │ ├── index.ts.ejs
│ │ └── types.ts.ejs
│ ├── README.md.ejs
│ └── .gitignore
├── scripts/ # Hook scripts
│ ├── pre-generate.js
│ └── post-generate.js
└── transformers/ # Schema transformers (optional)
└── default.jsMinimal Template Example
Here's a simple template that generates a "Hello World" SDK:
template.config.js
module.exports = {
name: "hello-world-template",
description: "A simple Hello World SDK template",
// Define prompts for user input
prompts: [
{
type: "input",
name: "greeting",
message: "What greeting would you like to use?",
default: "Hello, World!",
},
{
type: "input",
name: "author",
message: "Author name?",
default: "Palantir SDK Generator",
},
],
// Files to process as EJS templates
templateFiles: ["**/*.ejs"],
// Files to copy without processing
staticFiles: [".gitignore", "**/*.md"],
// Hooks for custom logic
hooks: {
beforeGenerate: "./scripts/pre-generate.js",
afterGenerate: "./scripts/post-generate.js",
},
};template/package.json.ejs
{
"name": "<%= projectName %>",
"version": "1.0.0",
"description": "SDK generated from schema",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"author": "<%= author %>",
"license": "<%= license %>",
"scripts": {
"build": "tsc",
"test": "jest"
},
"devDependencies": {
"typescript": "^5.0.0",
"@types/node": "^20.0.0"
}
}template/src/helloWorld.ts.ejs
/**
* Generated on <%= new Date().toISOString() %>
* Author: <%= author %>
*/
export class HelloWorld {
private greeting: string = '<%= greeting %>';
constructor(customGreeting?: string) {
if (customGreeting) {
this.greeting = customGreeting;
}
}
greet(name: string = 'World'): string {
return `${this.greeting}, ${name}!`;
}
// Schema information
getSchemaInfo(): object {
return <%- JSON.stringify(schema, null, 2) %>;
}
}
export default HelloWorld;scripts/pre-generate.js
module.exports = async function preGenerate(context) {
const { projectName, schema, answers, templateConfig } = context;
console.log(`🚀 Preparing to generate ${projectName}`);
console.log(`📋 Using template: ${templateConfig.name}`);
console.log(`👤 Author: ${answers.author}`);
// Validate schema
if (!schema || Object.keys(schema).length === 0) {
console.warn("⚠️ Warning: Schema is empty");
}
// Modify context if needed
context.additionalData = {
generatedAt: new Date().toISOString(),
nodeVersion: process.version,
};
return context;
};scripts/post-generate.js
const fs = require("fs-extra");
const path = require("path");
module.exports = async function postGenerate(context, outputPath) {
const { projectName, answers } = context;
console.log(`✨ Finalizing ${projectName}`);
// Create additional directories
await fs.ensureDir(path.join(outputPath, "dist"));
await fs.ensureDir(path.join(outputPath, "tests"));
// Create a simple test file
const testContent = `
import HelloWorld from '../src/helloWorld';
describe('HelloWorld', () => {
it('should greet correctly', () => {
const hw = new HelloWorld();
expect(hw.greet('SDK')).toBe('${answers.greeting}, SDK!');
});
});
`;
await fs.writeFile(
path.join(outputPath, "tests", "helloWorld.test.ts"),
testContent.trim(),
);
// Create TypeScript config
const tsConfig = {
compilerOptions: {
target: "ES2020",
module: "commonjs",
declaration: true,
outDir: "./dist",
rootDir: "./src",
strict: true,
esModuleInterop: true,
skipLibCheck: true,
forceConsistentCasingInFileNames: true,
},
include: ["src/**/*"],
exclude: ["node_modules", "dist", "tests"],
};
await fs.writeJSON(
path.join(outputPath, "tsconfig.json"),
tsConfig,
{ spaces: 2 },
);
console.log("📦 Package structure created successfully!");
};Hook Execution Model
Important: Template hooks run as separate Node.js processes in the template's directory context. This means:
- Dependency Isolation: Hooks have access to the template package's
node_modules, not the CLI's - Version Independence: The template can depend on a specific version of
@palantir/pack.sdkgenand other tools - Context Passing: Hook context is serialized to JSON and passed via command-line arguments
- Return Values: Hooks can return modified context (for
beforeGenerate) via stdout
Example hook that uses template dependencies:
// hooks/afterGenerate.js
import { execSync } from "child_process";
export default async function afterGenerate(context) {
const { outputPath, schemaPath } = context;
// This uses the template's own dependencies
const typeGenPath = require.resolve("@my-org/type-generator/bin/cli.js");
execSync(
`node "${typeGenPath}" -i "${schemaPath}" -o "${outputPath}/types"`,
{
stdio: "inherit",
cwd: outputPath,
},
);
}Advanced Template Features
Custom Schema Transformers
// transformers/default.js
module.exports = {
async transform(schema, context) {
// Transform the raw schema into a format your templates can use
return {
version: schema.version || "1.0.0",
endpoints: extractEndpoints(schema),
types: extractTypes(schema),
metadata: {
generatedAt: new Date().toISOString(),
sourceFile: context.schemaPath,
},
};
},
};
function extractEndpoints(schema) {
// Your schema parsing logic
return [];
}
function extractTypes(schema) {
// Your type extraction logic
return [];
}Template Variables
Templates have access to these variables:
projectName- The name of the project being createdschema- The transformed schema dataanswers- User responses to promptsutils- Helper functions (see below)- Any additional data added by hooks
Utility Functions
// Available in templates as `utils`
{
camelCase: (str) => string, // Convert to camelCase
pascalCase: (str) => string, // Convert to PascalCase
kebabCase: (str) => string, // Convert to kebab-case
snakeCase: (str) => string, // Convert to snake_case
pluralize: (str) => string, // Pluralize a word
singularize: (str) => string, // Singularize a word
capitalize: (str) => string, // Capitalize first letter
lower: (str) => string, // Convert to lowercase
upper: (str) => string // Convert to uppercase
}CLI Development Guide
Project Structure
sdkgen/
├── src/
│ ├── cli.ts # CLI entry point
│ ├── commands/
│ │ └── create.ts # Create command implementation
│ ├── core/
│ │ ├── generator.ts # Core generation engine
│ │ ├── template-loader.ts # Template loading logic
│ │ └── schema-parser.ts # Schema parsing utilities
│ ├── utils/
│ │ ├── prompts.ts # User prompt utilities
│ │ ├── logger.ts # Logging utilities
│ │ └── file-system.ts # File system helpers
│ └── types/
│ └── index.ts # TypeScript type definitions
├── templates/
│ └── default/ # Default template
├── bin/
│ └── cli.js # Executable entry point
├── package.json
├── tsconfig.json
└── README.mdImplementation Steps
Set up the project
mkdir sdkgen cd sdkgen npm init -y npm install commander inquirer ejs fs-extra glob chalk npm install -D typescript @types/node @types/inquirer @types/ejsCreate the CLI entry point
// src/cli.ts #!/usr/bin/env node import { Command } from 'commander'; import { create } from './commands/create'; const program = new Command(); program .name('sdkgen') .description('Generate SDKs from schema definitions') .version('1.0.0'); program .command('create <project-name>') .description('Create a new SDK project') .option('-t, --template <template>', 'template to use', 'default') .option('-s, --schema <path>', 'path to schema file') .option('--skip-install', 'skip dependency installation') .option('--verbose', 'enable verbose logging') .option('--dry-run', 'preview without writing files') .action(create); program.parse();Implement the generator core
// src/core/generator.ts import ejs from "ejs"; import fs from "fs-extra"; import glob from "glob"; import path from "path"; export class Generator { constructor(private context: GeneratorContext) {} async generate(outputPath: string): Promise<void> { // Implementation as shown in architecture section } }Build and link for testing
npm run build npm link npx @palantir/pack.sdkgen create test-project --schema ./test.json
API Reference
Generator Context
interface GeneratorContext {
projectName: string;
schema: any;
answers: Record<string, any>;
templateConfig: TemplateConfig;
schemaPath: string;
outputPath: string;
options: {
skipInstall?: boolean;
verbose?: boolean;
dryRun?: boolean;
};
}Template Configuration
interface TemplateConfig {
name: string;
description: string;
prompts?: PromptQuestion[];
templateFiles?: string[];
staticFiles?: string[];
hooks?: {
beforeGenerate?: string | Function;
afterGenerate?: string | Function;
beforeInstall?: string | Function;
afterInstall?: string | Function;
};
transformers?: {
[key: string]: string | Function;
};
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
}Hook Functions
// Pre-generate hook
type PreGenerateHook = (context: GeneratorContext) => Promise<GeneratorContext>;
// Post-generate hook
type PostGenerateHook = (
context: GeneratorContext,
outputPath: string,
) => Promise<void>;Examples
Creating a REST API Client Template
// template.config.js
module.exports = {
name: "rest-api-client",
prompts: [
{
type: "input",
name: "baseUrl",
message: "API base URL?",
default: "https://api.example.com",
},
{
type: "list",
name: "httpClient",
message: "HTTP client library?",
choices: ["fetch", "axios", "node-fetch"],
},
],
hooks: {
afterGenerate: async (context, outputPath) => {
// Install selected HTTP client
const { httpClient } = context.answers;
if (httpClient === "axios") {
// Add axios to package.json dependencies
}
},
},
};Using Environment-Specific Configuration
# Development
TEMPLATE_ENV=dev npx @palantir/pack.sdkgen create my-sdk \
--schema ./schema.json
# Production
TEMPLATE_ENV=prod npx @palantir/pack.sdkgen create my-sdk \
--schema ./schema.jsonTroubleshooting
Common Issues
Template not found
- Ensure the template package is published to npm
- Check that the template path is correct for local templates
- Verify the template has a valid
template.config.js
Schema parsing errors
- Validate your schema file is valid JSON/YAML
- Check that the schema transformer matches your schema format
- Use
--verboseflag for detailed error messages
Generation fails
- Run with
--dry-runfirst to preview changes - Check hook scripts for errors
- Ensure all required template variables are provided
