Files
winos-config/scripts/gen-dsc-resources-types.ts
EthanShoeDev 239f2ff7d3 script update
2025-12-05 17:09:59 -05:00

344 lines
9.7 KiB
TypeScript

import { Command, FileSystem, Path } from '@effect/platform';
import { BunContext, BunRuntime } from '@effect/platform-bun';
import { Effect, Logger, LogLevel, Schema as S } from 'effect';
import { jsonSchemaToEffectSchema } from './lib/script-utils';
// Schema for DSC resource list output
const DscResourceSchema = S.Struct({
type: S.String,
kind: S.String,
version: S.String,
capabilities: S.Array(S.String),
path: S.String,
description: S.NullOr(S.String),
directory: S.String,
manifest: S.Struct({
$schema: S.String,
type: S.String,
version: S.String,
description: S.optional(S.NullOr(S.String)),
schema: S.optional(
S.NullOr(
S.Union(
S.Struct({
command: S.Struct({
executable: S.String,
args: S.NullOr(S.Array(S.String)),
}),
}),
S.Struct({
embedded: S.Unknown,
}),
),
),
),
}),
});
type DscResource = S.Schema.Type<typeof DscResourceSchema>;
/**
* Sanitize a resource type name to be a valid TypeScript identifier and filename
* e.g., "Microsoft.WinGet/Package" -> "MicrosoftWinGetPackage"
*/
const sanitizeTypeName = (resourceType: string): string => {
return resourceType
.replace(/[/\\]/g, '') // Remove slashes
.replace(/\./g, '') // Remove dots
.replace(/[^a-zA-Z0-9]/g, ''); // Remove any other invalid chars
};
/**
* Convert resource type to a safe filename
* e.g., "Microsoft.WinGet/Package" -> "microsoft-winget-package"
*/
const toSafeFilename = (resourceType: string): string => {
return resourceType
.replace(/[/\\]/g, '-') // Replace slashes with hyphens
.replace(/\./g, '-') // Replace dots with hyphens
.toLowerCase();
};
/**
* Get schema for a DSC resource - either from embedded schema or via `dsc resource schema`
*/
const getDscResourceSchema = (resource: DscResource) =>
Effect.gen(function* () {
const schema = resource.manifest.schema;
if (!schema) {
yield* Effect.logDebug(`No schema defined for ${resource.type}`);
return null;
}
// Check if it's an embedded schema
if ('embedded' in schema) {
yield* Effect.logDebug(`Using embedded schema for ${resource.type}`);
return JSON.stringify(schema.embedded);
}
// Otherwise, use dsc resource schema command (more reliable than running executable directly)
if ('command' in schema) {
yield* Effect.logDebug(`Getting schema via DSC CLI for ${resource.type}`);
const schemaString = yield* Command.make(
'dsc',
'resource',
'schema',
'--resource',
resource.type,
).pipe(Command.string);
return schemaString.trim();
}
return null;
});
/**
* Parse NDJSON output (newline-delimited JSON) from dsc resource list
*/
const parseNdjson = (output: string): Array<unknown> =>
output
.split('\n')
.map((line) => line.trim())
.filter((line) => line.length > 0)
.map((line) => {
try {
return JSON.parse(line);
} catch {
return null;
}
})
.filter((obj): obj is unknown => obj !== null);
/**
* Get available DSC resources from the system
*/
const getAvailableDscResources = () =>
Effect.gen(function* () {
yield* Effect.logDebug('Getting available DSC resources...');
const dscrResourcesListOutput = yield* Command.make(
'dsc',
'resource',
'list',
'-o',
'json',
).pipe(Command.string);
// Parse NDJSON output
const rawResources = parseNdjson(dscrResourcesListOutput);
yield* Effect.log(`Found ${rawResources.length} resources in DSC output`);
// Decode each resource with the schema
const resources: Array<DscResource> = [];
for (const raw of rawResources) {
const decoded = S.decodeUnknownOption(DscResourceSchema)(raw);
if (decoded._tag === 'Some') {
resources.push(decoded.value);
} else {
// Log which resource failed to parse for debugging
const maybeType =
typeof raw === 'object' && raw !== null && 'type' in raw
? (raw as { type: unknown }).type
: 'unknown';
yield* Effect.logDebug(`Failed to decode resource: ${maybeType}`);
}
}
yield* Effect.log(`Successfully decoded ${resources.length} resources`);
return resources;
});
/**
* Generate index file that re-exports all types
*/
const generateIndexFile = (
resourceTypes: Array<{ type: string; filename: string; typeName: string }>,
) => {
const lines = [
'// This file is auto-generated. Do not edit manually.',
'// Re-exports all DSC resource schema types',
'',
];
for (const { filename, typeName } of resourceTypes) {
lines.push(`export * as ${typeName} from './${filename}';`);
}
lines.push('');
return lines.join('\n');
};
/**
* Generate a types list file for all DSC resource configurations
*/
const generateDscConfigTypes = (
resourceTypes: Array<{ type: string; filename: string; typeName: string }>,
) => {
const lines = [
'// This file is auto-generated. Do not edit manually.',
"import * as S from 'effect/Schema';",
'',
];
// Create a resource types literal
lines.push('/**');
lines.push(' * All available DSC resource types on this system');
lines.push(' */');
lines.push('export const DscResourceTypes = S.Literal(');
for (const { type } of resourceTypes) {
lines.push(` "${type}",`);
}
lines.push(');');
lines.push('');
lines.push(
'export type DscResourceType = S.Schema.Type<typeof DscResourceTypes>;',
);
lines.push('');
// Create a map of type to schema import
lines.push('/**');
lines.push(' * Map of resource type to generated file');
lines.push(' */');
lines.push('export const ResourceTypeToFile = {');
for (const { type, filename } of resourceTypes) {
lines.push(` "${type}": "${filename}.gen",`);
}
lines.push('} as const;');
lines.push('');
return lines.join('\n');
};
const genDscResourcesTypes = Effect.gen(function* () {
yield* Effect.log('Starting DSC resources types generation...');
const resources = yield* getAvailableDscResources();
// Filter to only resources that have schemas and are not adapters
// (adapters like PowerShell adapter don't have their own config schema)
const resourcesWithSchemas = resources.filter(
(r) => r.manifest.schema && r.kind !== 'adapter',
);
yield* Effect.log(
`Found ${resourcesWithSchemas.length} resources with schemas (excluding adapters)`,
);
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const outputDir = path.join(
process.cwd(),
'src',
'dsc-resource-schema-types',
);
yield* Effect.logDebug(`Output directory: ${outputDir}`);
if (yield* fs.exists(outputDir)) {
yield* Effect.log('Removing existing output directory...');
yield* fs.remove(outputDir, { recursive: true, force: true });
}
yield* fs.makeDirectory(outputDir, { recursive: true });
const generatedTypes: Array<{
type: string;
filename: string;
typeName: string;
}> = [];
const errors: Array<{ type: string; error: string }> = [];
for (const resource of resourcesWithSchemas) {
yield* Effect.logDebug(`Processing: ${resource.type}`);
const schemaResult = yield* Effect.either(getDscResourceSchema(resource));
if (schemaResult._tag === 'Left') {
errors.push({
type: resource.type,
error: String(schemaResult.left),
});
yield* Effect.logWarning(
`Failed to get schema for ${resource.type}: ${schemaResult.left}`,
);
continue;
}
const jsonSchema = schemaResult.right;
if (!jsonSchema) {
yield* Effect.logDebug(`No schema available for ${resource.type}`);
continue;
}
const typeName = sanitizeTypeName(resource.type);
const filename = toSafeFilename(resource.type);
const effectSchemaResult = yield* Effect.either(
jsonSchemaToEffectSchema({
jsonSchema,
name: typeName,
}),
);
if (effectSchemaResult._tag === 'Left') {
errors.push({
type: resource.type,
error: String(effectSchemaResult.left),
});
yield* Effect.logWarning(
`Failed to convert schema for ${resource.type}: ${effectSchemaResult.left}`,
);
continue;
}
const effectSchema = effectSchemaResult.right;
const outputPath = path.join(outputDir, `${filename}.gen.ts`);
yield* fs.writeFileString(outputPath, effectSchema);
generatedTypes.push({
type: resource.type,
filename,
typeName,
});
yield* Effect.log(`Generated: ${filename}.gen.ts (${resource.type})`);
}
// Generate index file
if (generatedTypes.length > 0) {
const indexContent = generateIndexFile(generatedTypes);
const indexPath = path.join(outputDir, 'index.ts');
yield* fs.writeFileString(indexPath, indexContent);
yield* Effect.log('Generated: index.ts');
// Generate types list file
const typesListContent = generateDscConfigTypes(generatedTypes);
const typesListPath = path.join(outputDir, '_resource-types.gen.ts');
yield* fs.writeFileString(typesListPath, typesListContent);
yield* Effect.log('Generated: _resource-types.gen.ts');
}
yield* Effect.log('');
yield* Effect.log('=== Generation Summary ===');
yield* Effect.log(`Successfully generated: ${generatedTypes.length} types`);
if (errors.length > 0) {
yield* Effect.log(`Failed: ${errors.length} types`);
for (const { type, error } of errors) {
yield* Effect.logDebug(` - ${type}: ${error}`);
}
}
yield* Effect.log('DSC types generation complete!');
});
BunRuntime.runMain(
genDscResourcesTypes.pipe(
Effect.provide(BunContext.layer),
Logger.withMinimumLogLevel(LogLevel.Debug),
Effect.scoped,
),
);