UNPKG

nihilqui

Version:

Typescript .d.ts generator from GIR for gjs and node-gtk

318 lines (293 loc) 12.5 kB
/** * The TemplateProcessor is used generate strings from templates files or template strings * For example, the signal methods are generated here */ import { existsSync } from 'fs' import { readFile, writeFile, mkdir, readdir } from 'fs/promises' import { join, dirname, relative, extname } from 'path' import ejs from 'ejs' import { __dirname } from './utils.js' import { Logger, APP_NAME, APP_USAGE, APP_SOURCE, APP_VERSION, PACKAGE_DESC, PACKAGE_KEYWORDS, getDestPath, DependencyManager, Transformation, } from '@ts-for-gir/lib' import type { GenerateConfig, Dependency, TemplateData, Environment } from '@ts-for-gir/lib' const TEMPLATE_DIR = join(__dirname, '../templates') export class TemplateProcessor { protected environmentTemplateDir: string protected log: Logger protected transformation: Transformation constructor( protected readonly data: TemplateData | undefined, protected readonly packageName: string, protected readonly deps: Dependency[], protected readonly config: GenerateConfig, ) { this.transformation = new Transformation(config) const dep = DependencyManager.getInstance(config) let outdir = config.outdir || './' // Make outdir relative to the root directory outdir = relative(config.root, outdir) const typeDir = getDestPath(outdir) this.data = { ...this.data, APP_NAME, APP_USAGE, APP_SOURCE, APP_VERSION, PACKAGE_DESC: PACKAGE_DESC(packageName, this.config.environment, this.data?.girModule?.libraryVersion), PACKAGE_KEYWORDS: PACKAGE_KEYWORDS(packageName, this.config.environment), importName: this.transformation.transformImportName(packageName), dep, deps, typeDir, join, dirname, } this.environmentTemplateDir = this.getEnvironmentDir(config.environment, TEMPLATE_DIR) this.log = new Logger(config.environment, config.verbose, this.packageName) } /** * Get the output or input directory of the environment * @param environment The environment to get the directory for * @param baseDir The base directory * @returns The path to the directory */ protected getEnvironmentDir = (environment: Environment, baseDir: string): string => { if (!baseDir.endsWith(environment)) if (environment === 'gjs' && !baseDir.endsWith('/gjs')) { return join(baseDir, 'gjs') } if (environment === 'node' && !baseDir.endsWith('/node-gtk')) { return join(baseDir, 'node-gtk') } return baseDir } protected getAppendTemplateName(templateFilename: string) { let appendTemplateFilename = templateFilename if (appendTemplateFilename.endsWith('.d.ts')) { appendTemplateFilename = appendTemplateFilename.replace('.d.ts', '.append.d.ts') } else if (extname(appendTemplateFilename)) { const ext = extname(appendTemplateFilename) appendTemplateFilename = appendTemplateFilename.replace(ext, '.append' + ext) } else { appendTemplateFilename += '.append' } return appendTemplateFilename } /** * Loads and renders a template and gets the rendered templates back * @param templateFilename */ public async load( templateFilename: string, options: Partial<ejs.Options> = {}, overrideTemplateData: TemplateData = {}, ): Promise<{ prepend: string; append: string }> { const fileContent = await this.read(templateFilename) const prepend = await this.render(fileContent, options, overrideTemplateData) let append = '' const appendTemplateFilename = this.getAppendTemplateName(templateFilename) if (this.exists(appendTemplateFilename)) { const appendFileContent = await this.read(appendTemplateFilename) append = await this.render(appendFileContent, options, overrideTemplateData) } return { prepend, append } } /** * Loads and renders all templates in a directory and gets the rendered templates back * @param templateDirname * @param fileExtension * @param options EJS options * @param overrideTemplateData Override template data if you want * @returns The rendered templates */ public async loadAll( templateDirname: string, fileExtension: string, options: Partial<ejs.Options> = {}, overrideTemplateData: TemplateData = {}, ): Promise<{ [path: string]: string }> { const fileContents = await this.readAll(templateDirname, fileExtension) for (const file of Object.keys(fileContents)) { fileContents[file] = await this.render(fileContents[file], options, overrideTemplateData) } return fileContents } /** * Loads an template, render the template and write the template to the filesystem * @param templateFilename The filename of the template * @param baseOutputPath The base output directory path where the templates should be written to * @param outputFilename The filename of the output file * @param prependEnv A (optional) boolean that indicates if the environment should be prepended to the output path * @param content A (optional) string that should be appended to the rendered template * @param options EJS options * @param overrideTemplateData Override template data if you want * @return The rendered template string */ public async create( templateFilename: string, baseOutputPath: string, outputFilename: string, prependEnv = true, content = '', options: Partial<ejs.Options> = {}, overrideTemplateData: TemplateData = {}, ): Promise<string> { const { prepend, append } = await this.load(templateFilename, options, overrideTemplateData) const code = prepend + '\n' + content + '\n' + append await this.write(code, baseOutputPath, outputFilename, prependEnv) return code } /** * Loads all templates with file extension in dir, render the templates and write the template to the filesystem * @param fileExtension The file extension of the templates * @param templateDirname The directory where the templates are located * @param baseOutputPath The base output directory path where the templates should be written to * @param outputDirname The child output directory of the base output directory where the templates should be written to * @param prependEnv A (optional) boolean that indicates if the environment should be prepended to the output path * @param append A (optional) string that should be appended to the rendered template * @param options EJS options * @param overrideTemplateData Override template data if you want * @return The rendered (and if possible prettified) templates */ public async createAll( fileExtension: string, templateDirname: string, baseOutputPath: string, outputDirname: string, prependEnv = true, append = '', options: Partial<ejs.Options> = {}, overrideTemplateData: TemplateData = {}, ) { const rendered = await this.loadAll(templateDirname, fileExtension, options, overrideTemplateData) const result: { [path: string]: string } = {} for (const filename of Object.keys(rendered)) { const destPath = getDestPath(baseOutputPath, outputDirname, filename) result[destPath] = rendered[filename] + '\n' + append await this.write(result[destPath], baseOutputPath, join(outputDirname, filename), prependEnv) } return result } public getOutputPath(baseOutputPath: string, outputFilename: string, prependEnv = true): string { const filePath = this.config.package ? join(this.data?.importName || this.packageName, outputFilename) : outputFilename const outputPath = prependEnv ? getDestPath(baseOutputPath, filePath) : join(baseOutputPath, filePath) return outputPath } /** * Writes the `content` to the filesystem * @param content The content (normally the content of a rendered template file) that should be written to the filesystem * @param baseOutputPath The base output directory path where the templates should be written to * @param outputFilename The filename of the output file * @param prependEnv A (optional) boolean that indicates if the environment should be prepended to the output path * @returns */ protected async write( content: string, baseOutputPath: string, outputFilename: string, prependEnv = true, ): Promise<string> { const outputPath = this.getOutputPath(baseOutputPath, outputFilename, prependEnv) // write template result file await mkdir(dirname(outputPath), { recursive: true }) await writeFile(outputPath, content, { encoding: 'utf8', flag: 'w' }) return Promise.resolve(outputPath) } /** * * @param templateString The template content string that should be rendered * @param options EJS options * @param overrideTemplateData Override template data if you want * @returns */ protected async render( templateString: string, options: Partial<ejs.Options> = {}, overrideTemplateData: TemplateData = {}, ): Promise<string> { try { const renderedTpl = await ejs.render( templateString, { ...this.config, ...this.data, packageName: this.packageName, ...overrideTemplateData, }, { async: true, ...options, }, ) return renderedTpl } catch (error) { console.error(error) this.log.error('Error on render', error) return '' } } /** * Checks if the template file or directory exists and returns the path if found * Tries first to load the file / directory from the environment-specific template folder and otherwise looks for it in the general template folder * @param templateFilename */ public exists(templateFilename: string): string | null { const fullEnvironmentTemplatePath = join(this.environmentTemplateDir, templateFilename) const fullGeneralTemplatePath = join(TEMPLATE_DIR, templateFilename) if (existsSync(fullEnvironmentTemplatePath)) { return fullEnvironmentTemplatePath } if (existsSync(fullGeneralTemplatePath)) { return fullGeneralTemplatePath } return null } /** * Reads a template file from filesystem and gets the raw string back * @param templateFilename * @return The raw template content */ protected async read(templateFilename: string) { const path = this.exists(templateFilename) if (path) { return await readFile(path, 'utf8') } throw new Error(`Template '${templateFilename}' not found'`) } /** * Reads all template files from a directory and gets the raw strings back * @param templateDirname * @param fileExtension * @return The raw template contents * @throws Error if the template directory does not exist * @throws Error if the template directory is empty */ protected async readAll(templateDirname: string, fileExtension: string) { const path = this.exists(templateDirname) if (path) { const files = (await readdir(path)).filter((file) => file.endsWith(fileExtension)) if (files.length === 0) { throw new Error(`Template directory '${templateDirname}' is empty'`) } const results: { [path: string]: string } = {} for (const file of files) { results[file] = await readFile(join(path, file), 'utf8') } return results } throw new Error(`Template directory '${templateDirname}' not found'`) } } export default TemplateProcessor