limgen
Version:
Infrastructure as Code generator
214 lines (187 loc) • 7.08 kB
text/typescript
import { promises as fs } from "fs";
import path from "path";
import { generatePackageJSON, generateTSConfig } from "./workspace";
import { copyFileDependencies } from "./files";
import { installDependencies } from "./npm";
import { initOptionsSchema } from "./commands/init";
import { z } from "zod";
import { parseYaml } from "./yaml";
/**
* Represents the type of a project.
*
* 'fullstack-aws' - A fullstack project using AWS Fargate.
* 'staticsite-aws' - A static site project using AWS services.
* 'fullstack-azure' - A fullstack project using Azure Container Apps.
*/
export type ProjectType = 'fullstack-aws'|'staticsite-aws'|'fullstack-azure';
export type BaseProjectInputOptions = {
projectName: string;
projectType: ProjectType;
}
/**
* Represents information about the dependencies of a project.
*
* @typedef {Object} ProjectDependencyInfo
* @property {string[]} components - An array of component names that the project depends on.
* @property {string[]} packages - An array of package names that the project depends on.
*/
export type ProjectDependencyInfo = {
files: string[];
packages: string[];
}
/**
* Represents a Limgen project with customizable options.
*
* @template TOpts - The type of options that can be passed to the project methods.
*/
export type LimgenProject<TOpts = unknown> = {
inputs(initArgs: { name: string; }): Promise<{
name: string;
message: string;
schema: any;
}[]>,
/**
* Renders a project template with the provided options.
*
* @param opts - Optional parameters for the default action.
* @returns A promise that resolves to a string result of the default action.
*/
default: (opts?: TOpts) => Promise<string>;
/**
* Determines the dependencies of the project.
*
* @param opts - Optional parameters for determining dependencies.
* @returns A promise that resolves to an array of strings representing the dependencies.
*/
dependsOn: (opts?: TOpts) => Promise<ProjectDependencyInfo>;
/**
* Collects input required to initialize the project.
*
* @param initArgs - Initial arguments including project name and framework.
* @param argv - Additional arguments provided as a record of key-value pairs.
* @returns A promise that resolves to the collected options.
*/
collectInput: (initArgs: {
projectName: string;
}, opts: any) => Promise<TOpts>;
/**
* The type of the project.
*/
type: ProjectType;
/**
* The name of the project.
*/
name: string;
/**
* Pulls environment variables for the project.
*
* @param project - The name of the project to pull environment variables from.
* @param stack - The name of the stack to pull environment variables from.
* @returns A promise that resolves when the environment variables have been pulled.
* @throws An error if the environment variables could not be pulled.
* @remarks This method is optional and may not be implemented by all projects.
* @example
* ```typescript
* await project.envPull('my-project', 'dev');
* ```
*/
envPull?: (opts: { projectName: string, stack: string }) => Promise<void>;
}
/**
* An array containing all possible project types.
*/
export const AllProjectTypes = ['fullstack-aws', 'staticsite-aws', 'fullstack-azure'] as const;
/**
* Imports a project module dynamically based on the provided project type.
*
* @param projectType - The type of the project to import.
* @returns A promise that resolves to the imported project module.
*/
export async function importProjectType(projectType: ProjectType): Promise<LimgenProject> {
return await import(`./project-types/${projectType}/project`);
}
/**
* Initializes a project folder within the 'infrastructure' directory.
* Creates the directory recursively if it does not exist.
*
* @param projectName - The name of the project for which the folder is to be created.
* @returns A promise that resolves when the directory has been created.
*/
export async function ensureProjectFolder(projectName: string) {
await fs.mkdir(path.join('infrastructure', 'projects', projectName), { recursive: true });
}
/**
* Generates the index file for the given project.
*
* @param project - The LimgenProject instance for which the index file is to be generated.
* @param opts - Options to be passed to the project's default method.
* @returns A promise that resolves when the index file has been written.
*/
export async function applyProject(project: LimgenProject, inputs: any) {
await ensureProjectFolder(inputs.projectName);
const result = await project.default(inputs);
await fs.writeFile(path.join('infrastructure', 'projects', inputs.projectName, 'index.ts'), result);
}
/**
* Options for generating a project YAML file.
*/
export interface ProjectYamlOptions {
projectType: any;
framework: any;
projectName: string;
}
/**
* Generates a Pulumi project YAML file with the provided options.
*
* @param opts - The options to use when generating the project YAML file.
* @returns A promise that resolves when the project YAML file has been written.
*/
export async function generateProjectYaml(opts: ProjectYamlOptions & Record<string, any>) {
const { projectName, projectType: type, framework, ...projectInputs } = opts;
await ensureProjectFolder(opts.projectName);
const yaml = `
name: ${opts.projectName}
description: A pulumi project created with limgen
runtime:
name: nodejs
options:
packagemanager: pnpm
config:
pulumi:tags:
value:
pulumi:template: aws-typescript
limgen:
projectName: ${opts.projectName}
projectType: ${opts.projectType}${opts.framework ? `
framework: ${opts.framework}` : ''}${Object.keys(projectInputs).length ? `
projectInputs:${Object.entries(projectInputs).map(([key, value]) => `
${key}: ${value}`).join('')}` : ''}
`.trimStart()
await fs.writeFile(path.join('infrastructure', 'projects', opts.projectName, 'Pulumi.yaml'), yaml);
}
export async function renderProject(cmdArgs: z.infer<typeof initOptionsSchema>, project: LimgenProject, inputs: any) {
const { packages, files } = await project.dependsOn(inputs);
await Promise.all([
copyFileDependencies(files),
generatePackageJSON().then(() => installDependencies(cmdArgs.directory, packages)),
generateTSConfig(),
generateProjectYaml(inputs),
applyProject(project, inputs),
])
}
export async function readProjectMetadata(opts: { projectName: string; }) {
const pulumiYamlPath = path.join('infrastructure', 'projects', opts.projectName, 'Pulumi.yaml');
const pulumiYaml = await fs.readFile(pulumiYamlPath, 'utf-8');
// get the settings under the limgen key
if (!/limgen:(\n\s+.*)+/g.test(pulumiYaml)) {
throw new Error(`Could not find limgen settings in ${pulumiYamlPath}`);
}
const { limgen } = parseYaml(pulumiYaml) as any;
const limgenSchema = z.object({
projectName: z.string(),
projectType: z.string(),
framework: z.string().optional(),
projectInputs: z.record(z.unknown()),
});
return limgenSchema.parse(limgen);
}