@alauda/doom
Version:
Doctor Doom making docs.
159 lines (158 loc) • 6.62 kB
JavaScript
import fs from 'node:fs/promises';
import path from 'node:path';
import * as prompts from '@inquirer/prompts';
import { logger } from '@rsbuild/core';
import { Command } from 'commander';
import { render } from 'ejs';
import { glob } from 'tinyglobby';
import { cyan, magenta } from 'yoctocolors';
import { JS_STR_FALSY_VALUES } from '../shared/index.js';
import { resolveRepo, resolveStaticConfig } from '../utils/index.js';
export const DEFAULT_PATH = 'templates/scaffolding.yaml';
const scaffoldingTemplates = {
'product-doc': {
repo: 'alauda-public/product-doc-guide',
},
};
const getScaffoldings = async (repoFolder, scaffoldingPath) => {
const scaffoldingFile = path.resolve(repoFolder, scaffoldingPath);
try {
const stat = await fs.stat(scaffoldingFile);
if (stat.isFile()) {
const { scaffolding } = await resolveStaticConfig(scaffoldingFile);
return scaffolding;
}
}
catch (err_) {
const err = err_;
if (err.name === 'YAMLParseError') {
logger.error(`Failed to parse \`${magenta(scaffoldingPath)}\`: ${err.message}`);
}
}
};
const resolveScaffoldings = async (name, force) => {
if (!Object.hasOwn(scaffoldingTemplates, name)) {
logger.error(`Template \`${magenta(name)}\` not found, current available templates are: ${Object.keys(scaffoldingTemplates)
.map((t) => cyan(`\`${t}\``))
.join(', ')}`);
return;
}
const { repo, scaffoldingPath = 'templates/scaffolding.yaml' } = scaffoldingTemplates[name];
const repoFolder = await resolveRepo(repo, force);
if (!repoFolder) {
return;
}
const base = path.resolve(repoFolder, scaffoldingPath, '..');
const scaffoldings = await getScaffoldings(repoFolder, scaffoldingPath);
return { base, scaffoldings, repoFolder };
};
const handleTemplateFile = async ({ source, target, processors, parameters, writeMode = 'write', }) => {
let content = await fs.readFile(source, 'utf-8');
for (const processor of processors || []) {
switch (processor.type) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
case 'ejsTemplate': {
content = render(content, { data: processor.data, parameters });
break;
}
default: {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unknown processor type: \`${processor.type}\``);
}
}
}
await fs.mkdir(path.dirname(target), { recursive: true });
await fs[`${writeMode}File`](target, content);
};
export const newCommand = new Command('new')
.description('Generate scaffolding from templates')
.argument('[template]', 'Scaffolding template name, format: `[name][:type]`')
.action(async function (template = '') {
let [name, type] = template.split(':');
if (!name) {
name = 'product-doc';
}
const { force } = this.optsWithGlobals();
const { base, repoFolder, scaffoldings } = (await resolveScaffoldings(name, force)) || {};
if (!scaffoldings) {
if (repoFolder) {
logger.error(`Unable to resolve any scaffoldings, if you are sure the template \`${magenta(name)}\` exists, try to use the \`--force\` option, or event remove the local cache at ${magenta(repoFolder)} and try again`);
}
return;
}
if (!type) {
type = await prompts.select({
message: '请选择脚手架类型',
choices: scaffoldings.map((s) => ({
value: s.name,
description: s.description,
})),
});
}
const scaffolding = scaffoldings.find((s) => s.name === type);
if (!scaffolding) {
logger.error(`Scaffolding \`${magenta(type)}\` not found, current available scaffoldings are: ${scaffoldings
.map((s) => cyan(`\`${s.name}\``))
.join(', ')}`);
return;
}
const parameters = {};
for (const param of scaffolding.parameters || []) {
parameters[param.name] =
await // @ts-expect-error -- no idea how to fix this
prompts[param.type](param.options);
}
logger.start('Generating scaffolding...');
for (const layout of scaffolding.layout || []) {
const source = path.resolve(base, render(layout.source, { parameters }));
const target = path.resolve(render(layout.target, { parameters }));
const when = layout.when && render(layout.when, { parameters });
if (when != null && JS_STR_FALSY_VALUES.has(when)) {
continue;
}
switch (layout.type) {
case 'file': {
await handleTemplateFile({
source,
target,
parameters,
processors: layout.processors,
writeMode: layout.writeMode,
});
break;
}
case 'folder': {
const dirents = await fs.readdir(source, {
recursive: true,
withFileTypes: true,
});
const files = new Set(dirents
.filter((d) => d.isFile())
.map((d) => path.relative(source, path.resolve(d.parentPath ||
// eslint-disable-next-line @typescript-eslint/no-deprecated
d.path, d.name))));
for (const matcher of layout.matchers || []) {
const matched = await glob(matcher.match.map((m) => render(m, { parameters })), { cwd: source });
for (const file of matched) {
files.delete(file);
await handleTemplateFile({
source: path.resolve(source, file),
target: path.resolve(target, file.endsWith('.ejs') ? file.slice(0, -4) : file),
parameters,
processors: matcher.processors,
writeMode: matcher.writeMode,
});
}
}
for (const file of files) {
await handleTemplateFile({
source: path.resolve(source, file),
target: path.resolve(target, file),
});
}
break;
}
}
}
logger.success('Scaffolding generated successfully!');
});