templates-mo
Version:
Templates is a scaffolding framework that makes code generation simple, dynamic, and reusable. Generate files, parts of your app, or whole project structures—without the repetitive copy-pasting
174 lines (150 loc) • 4.05 kB
text/typescript
/**
* Inspired from hey-api/openapi-ts
* https://github.com/hey-api/openapi-ts/blob/main/packages/openapi-ts/src/index.ts
*/
import { sync } from 'cross-spawn';
import type { SettingsFilePrompt } from '@tps/types/settings';
import path from 'path';
import Templates from '@tps/templates';
import { findUp } from '@tps/utilities/fileSystem';
const NONE = 'none';
type ExcludeNone<T extends string> = Exclude<T, typeof NONE>;
export type OutputProcessor = {
args: (paths: string[]) => ReadonlyArray<string>;
command: string;
name: string;
};
type OutputProcessorMap<T extends string> = Record<
ExcludeNone<T>,
OutputProcessor
>;
function isErrnoException(error: unknown): error is NodeJS.ErrnoException {
return (error as NodeJS.ErrnoException).code !== undefined;
}
export const FORMATTER_PROMPT = {
name: 'formatter',
aliases: ['format'],
description:
'Type of formatter you would like to use to format the component',
message: 'What type of formatter do you want to use to format your code',
type: 'list',
tpsType: 'data',
hidden: true,
choices: [NONE, 'prettier', 'biome'],
default: NONE,
} as const satisfies SettingsFilePrompt;
export type formatters = (typeof FORMATTER_PROMPT.choices)[number];
/**
* Map of supported formatters
*/
const formattersMap: OutputProcessorMap<formatters> = {
biome: {
args: (paths) => ['format', '--write', ...paths],
command: 'biome',
name: 'Biome (Format)',
},
prettier: {
args: (paths) => [
'--ignore-unknown',
...paths,
'--write',
'--ignore-path',
'./.prettierignore',
],
command: 'prettier',
name: 'Prettier',
},
};
export const runFormatter = async (
formatter: formatters,
cwd: string,
paths: string[],
tps?: Templates,
): Promise<void> => {
if (formatter === NONE) {
return;
}
const formatterObj = formattersMap[formatter] ?? null;
if (formatterObj) {
runCommand(formatterObj, cwd, paths, tps);
}
};
/**
* Map of supported linters
*/
export const linters = {
biome: {
args: (paths) => ['lint', '--apply', ...paths],
command: 'biome',
name: 'Biome (Lint)',
},
eslint: {
args: (paths) => [...paths, '--fix'],
command: 'eslint',
name: 'ESLint',
},
} as const satisfies Record<string, OutputProcessor>;
export const runCommand = (
module: OutputProcessor,
cwd: string,
paths: string[],
tps: Templates = null,
): void => {
console.log(`✨ Running ${module.name}`);
const templatesNodeModulesBin = path.resolve(
__dirname,
'../../',
'node_modules',
'.bin',
);
// TODO: Use find up
// const destNodeModulesBin = path.resolve(cwd, 'node_modules', '.bin');
const parentNodeModules = findUp('node_modules', cwd);
const destNodeModulesBin = parentNodeModules
? [path.resolve(parentNodeModules, '.bin')]
: [];
const templateNodeModuleBin = tps
? [path.resolve(tps.src, 'node_modules', '.bin')]
: [];
const bins = [
// Bins from main template directory which satisfies default templates
templatesNodeModulesBin,
// Bins from Modules from the `cwd` or in other words `dest`
...destNodeModulesBin,
// Bins from template modules
...templateNodeModuleBin,
process.env.PATH,
];
const envPath = bins.join(':');
const result = sync(module.command, module.args(paths), {
env: { ...process.env, PATH: envPath },
});
if (result?.error) {
const { error } = result;
if (isErrnoException(error)) {
if (error.code === 'ENOENT') {
console.log(`❌ Command not found: ${module.command}`);
return;
}
}
console.log(`❌ ${module.name} failed!`);
console.log(error);
} else if (result?.status > 0) {
// command error
console.log(`❌ ${module.name} failed!`);
console.log(result.stdout.toString());
console.log(result.stderr.toString());
}
};
export const packageManagers = {
npm: {
args: (paths) => ['install', '--prefix', ...paths],
command: 'npm',
name: 'Npm Install',
},
yarn: {
args: (paths) => ['install', '--cwd', ...paths],
command: 'yarn',
name: 'Yarn Install',
},
} as const satisfies Record<string, OutputProcessor>;