diff --git a/sdk/typescript-schema/README.md b/sdk/typescript-schema/README.md index d4ab94e..6d1c147 100644 --- a/sdk/typescript-schema/README.md +++ b/sdk/typescript-schema/README.md @@ -79,6 +79,16 @@ npm run gen-schema This ensures that all generated files are up to date with your changes. +### Generating Minified Types + +To generate a single minified TypeScript file containing all type definitions from `audience.ts` and its dependencies: + +```bash +npm run gen-minified-types +``` + +This will create `schema/audience-types.min.ts` with all types consolidated into a single file without imports, exports, or comments. This is useful for documentation, distribution, or embedding in other tools. + ### Updating Title Paths in add_titles_to_schema.sh If you add, remove, or rename types or titles in the schema, you will need to update the relevant JSONPath mappings in [`scripts/add_titles_to_schema.sh`](./scripts/add_titles_to_schema.sh) to reflect these changes. This script relies on hardcoded paths to insert titles into the schema, so keeping these paths accurate is necessary for correct schema generation. diff --git a/sdk/typescript-schema/package.json b/sdk/typescript-schema/package.json index f54496c..e7e4ad4 100644 --- a/sdk/typescript-schema/package.json +++ b/sdk/typescript-schema/package.json @@ -4,6 +4,14 @@ "description": "TypeScript schema definitions for mParticle Audiences", "main": "dist/index.js", "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./schema.json": "./dist/schema/audience-schema.json", + "./types.min": "./dist/schema/audience-types.min.ts" + }, "files": [ "dist" ], @@ -12,9 +20,10 @@ "node": ">=18.20.4" }, "scripts": { - "build": "tsc", + "build": "tsc && mkdir -p dist/schema && cp schema/audience-schema.json dist/schema/ && cp schema/audience-types.min.ts dist/schema/", "semantic-release": "semantic-release", - "gen-schema": "scripts/gen_schema_with_titles.sh", + "gen-minified-types": "npx ts-node scripts/generate-minified-types.ts", + "gen-schema": "scripts/gen_schema_with_titles.sh && yarn gen-minified-types", "test": "jest", "test:watch": "jest --watch" }, @@ -32,4 +41,4 @@ "typescript": "5.0.4" }, "sideEffects": false -} +} \ No newline at end of file diff --git a/sdk/typescript-schema/schema/README.md b/sdk/typescript-schema/schema/README.md new file mode 100644 index 0000000..c2b5346 --- /dev/null +++ b/sdk/typescript-schema/schema/README.md @@ -0,0 +1,5 @@ +# audience-schema.json +JSON schema for an audience. Used to generate python. + +# audience-types.min.ts +A "minified" typescript file that includes all the types/enums/etc for the current audience definition. Useful when needing to pass this structure to LLMs with minimum characters. \ No newline at end of file diff --git a/sdk/typescript-schema/schema/audience-types.min.ts b/sdk/typescript-schema/schema/audience-types.min.ts new file mode 100644 index 0000000..345956d --- /dev/null +++ b/sdk/typescript-schema/schema/audience-types.min.ts @@ -0,0 +1 @@ +export const CURRENT_VERSION = '1.3.3'; type Version = `${number}.${number}.${number}` | `${number}.${number}.${number}-${string}`; type DateUnit = 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'; type AbsoluteDate = { 'absolute': string }; type RelativeDate = { 'relative': { offset: number, unit: DateUnit, boundary?: 'start' | 'end' } }; type DateOperand = { date: AbsoluteDate } | { date: RelativeDate }; type ModelPath = { model: string, path: string }; enum UnaryOperator { Null = 'null', NotNull = 'not_null', Exists = 'exists', NotExists = 'not_exists' }; type BinaryOperator = 'equals' | 'not_equals' | 'less_than' | 'less_than_equal' | 'greater_than' | 'greater_than_equal' | 'matches' | 'contains' | 'not_contains' | 'starts_with' | 'not_starts_with' | 'ends_with' | 'not_ends_with' | 'in' | 'not_in'; type ListOperator = 'contains' | 'between' | 'match_any' | 'match_all' | 'in' | 'not_in'; type AudienceOperator = 'in' | 'not_in'; type ArithmeticOperator = 'plus' | 'minus' | 'multiply' | 'divide' | 'mod'; type AggregationOperator = 'min' | 'max' | 'sum' | 'avg' | 'list' | 'count'; type LocationOperator = 'within' | 'equals'; type LogicalOperator = 'and' | 'or'; type AudienceOperand = { audience: string }; type ModelOperand = { model: string }; type Operand = boolean | number | string | DateOperand | ModelPath | ModelOperand | AudienceOperand | { operator: AggregationOperator, group_by_model: string, operand: Operand, condition?: Expression }; type Expression = { operator: UnaryOperator, operand: Operand } | { operator: BinaryOperator, left: Operand, right: Operand } | { operator: LogicalOperator, expressions: Expression[] }; type Audience = { schema_version: Version, audience: Expression } \ No newline at end of file diff --git a/sdk/typescript-schema/scripts/generate-minified-types.ts b/sdk/typescript-schema/scripts/generate-minified-types.ts new file mode 100644 index 0000000..bef83fc --- /dev/null +++ b/sdk/typescript-schema/scripts/generate-minified-types.ts @@ -0,0 +1,262 @@ +#!/usr/bin/env ts-node + +import * as fs from 'fs'; +import * as path from 'path'; + +interface ImportInfo { + imported: string[]; + from: string; +} + +/** + * Extracts import statements from a TypeScript file + */ +function extractImports(content: string): ImportInfo[] { + const imports: ImportInfo[] = []; + const importRegex = /import\s+(?:{([^}]+)}|(\w+))\s+from\s+['"]([^'"]+)['"]/g; + + let match; + while ((match = importRegex.exec(content)) !== null) { + const namedImports = match[1]; + const defaultImport = match[2]; + const from = match[3]; + + if (namedImports) { + const imported = namedImports.split(',').map(i => i.trim()); + imports.push({ imported, from }); + } else if (defaultImport) { + imports.push({ imported: [defaultImport], from }); + } + } + + return imports; +} + +/** + * Removes import and export keywords from content, and filters out non-type declarations + */ +function removeImportsAndExports(content: string): string { + // Remove import statements + content = content.replace(/import\s+(?:{[^}]+}|\w+)\s+from\s+['"][^'"]+['"];?\n?/g, ''); + + // Remove export keyword but keep the type/enum/interface declarations only + content = content.replace(/export\s+(type|enum|interface)/g, '$1'); + + // Split content into lines and filter out non-type declarations + const lines = content.split('\n'); + const filteredLines: string[] = []; + let inConstDeclaration = false; + let braceCount = 0; + + for (const line of lines) { + // Check if this line starts a const, class, or function declaration + if (/^(export\s+)?(const|class|function)\s+/.test(line.trim())) { + inConstDeclaration = true; + braceCount = (line.match(/{/g) || []).length - (line.match(/}/g) || []).length; + if (braceCount === 0 && line.includes(';')) { + // Single line const declaration + inConstDeclaration = false; + } + continue; // Skip this line + } + + if (inConstDeclaration) { + // Count braces to know when the declaration ends + braceCount += (line.match(/{/g) || []).length; + braceCount -= (line.match(/}/g) || []).length; + + if (braceCount <= 0) { + inConstDeclaration = false; + } + continue; // Skip this line + } + + // Keep type, enum, and interface declarations + filteredLines.push(line); + } + + content = filteredLines.join('\n'); + + return content; +} + +/** + * Removes comments from TypeScript code + */ +function removeComments(content: string): string { + // Remove multi-line comments (including JSDoc) + content = content.replace(/\/\*\*?[\s\S]*?\*\//g, ''); + + // Remove single-line comments + content = content.replace(/\/\/.*$/gm, ''); + + return content; +} + +/** + * Minifies TypeScript content by removing extra whitespace + */ +function minify(content: string): string { + // Remove multiple blank lines + content = content.replace(/\n\s*\n\s*\n/g, '\n'); + + // Remove trailing whitespace + content = content.replace(/[ \t]+$/gm, ''); + + // Remove leading whitespace from lines (but preserve indentation structure) + // This is conservative to maintain readability + + return content.trim(); +} + +/** + * Converts multi-line content to a single line with semicolons + */ +function convertToSingleLine(content: string): string { + // Remove all newlines and excessive whitespace + content = content.replace(/\n+/g, ' '); + + // Collapse multiple spaces into one + content = content.replace(/\s+/g, ' '); + + // Ensure proper semicolon separation between statements + // Add semicolon before type/interface/enum keywords if not already present + content = content.replace(/\s+(type|interface|enum)\s+/g, '; $1 '); + + // Add semicolon after closing braces if not already present + content = content.replace(/}\s*(?=[a-zA-Z])/g, '}; '); + + // Clean up any double semicolons + content = content.replace(/;+/g, ';'); + + // Clean up semicolon at the start if present + content = content.replace(/^\s*;\s*/, ''); + + // Remove spaces around semicolons for compactness + content = content.replace(/\s*;\s*/g, '; '); + + // Replace all double quotes with single quotes + content = content.replace(/"/g, "'"); + + return content.trim(); +} + +/** + * Resolves a relative import path to an absolute file path + */ +function resolveImportPath(currentFilePath: string, importPath: string): string { + const currentDir = path.dirname(currentFilePath); + const resolved = path.resolve(currentDir, importPath); + + // Try with .ts extension if it doesn't exist + if (fs.existsSync(resolved + '.ts')) { + return resolved + '.ts'; + } + if (fs.existsSync(resolved)) { + return resolved; + } + + throw new Error(`Cannot resolve import: ${importPath} from ${currentFilePath}`); +} + +/** + * Recursively collects all type definitions from a file and its imports + */ +function collectTypeDefinitions( + filePath: string, + visited: Set = new Set(), + definitions: Map = new Map() +): Map { + // Avoid circular dependencies + const absolutePath = path.resolve(filePath); + if (visited.has(absolutePath)) { + return definitions; + } + visited.add(absolutePath); + + // Read the file + const content = fs.readFileSync(absolutePath, 'utf-8'); + + // Extract imports and recursively process them first + const imports = extractImports(content); + for (const importInfo of imports) { + try { + const importedFilePath = resolveImportPath(absolutePath, importInfo.from); + collectTypeDefinitions(importedFilePath, visited, definitions); + } catch (e) { + console.warn(`Warning: Could not resolve import ${importInfo.from} in ${filePath}`); + } + } + + // Process current file content + let processedContent = removeImportsAndExports(content); + processedContent = removeComments(processedContent); + processedContent = minify(processedContent); + + // Only add if there's actual content + if (processedContent.trim()) { + definitions.set(absolutePath, processedContent); + } + + return definitions; +} + +/** + * Extracts the version value from version.ts + */ +function extractVersion(rootDir: string): string { + const versionFile = path.join(rootDir, 'version.ts'); + try { + const content = fs.readFileSync(versionFile, 'utf-8'); + const versionMatch = content.match(/VERSION\s*=\s*['"]([^'"]+)['"]/); + if (versionMatch) { + return versionMatch[1]; + } + } catch (e) { + console.warn('Warning: Could not read version from version.ts'); + } + return 'unknown'; +} + +/** + * Main function + */ +function main() { + const rootDir = path.resolve(__dirname, '..'); + const audienceFile = path.join(rootDir, 'audience.ts'); + const outputDir = path.join(rootDir, 'schema'); + const outputFile = path.join(outputDir, 'audience-types.min.ts'); + + console.log('Collecting type definitions from:', audienceFile); + + // Collect all type definitions + const definitions = collectTypeDefinitions(audienceFile); + + // Extract version + const version = extractVersion(rootDir); + + // Prepend version constant and combine all definitions + const versionExport = `export const CURRENT_VERSION = "${version}";`; + const allDefinitions = Array.from(definitions.values()).join('\n'); + const combined = versionExport + '\n' + allDefinitions; + + // Convert to single line with semicolons + const singleLine = convertToSingleLine(combined); + + // Ensure output directory exists + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + // Write the output + fs.writeFileSync(outputFile, singleLine, 'utf-8'); + + console.log(`Generated minified types at: ${outputFile}`); + console.log(`Version: ${version}`); + console.log(`Total files processed: ${definitions.size}`); + console.log(`Output size: ${singleLine.length} characters`); +} + +// Run the script +main(); + diff --git a/sdk/typescript-schema/tsconfig.json b/sdk/typescript-schema/tsconfig.json index 65dc70b..f672e0d 100644 --- a/sdk/typescript-schema/tsconfig.json +++ b/sdk/typescript-schema/tsconfig.json @@ -30,6 +30,7 @@ "exclude": [ "node_modules", "dist", - "scripts" + "scripts", + "schema" ] }