#!/usr/bin/env node
import fs from 'fs-extra';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import * as p from '@clack/prompts';
import { bold, cyan, grey, red, blue } from 'kleur/colors';
import minimist from 'minimist';
import pacote from 'pacote';
import * as langAll from './lang';
// 获取最新版本号
// Get the latest version number
const getLatestVersion = async packageName => {
const manifest = await pacote.manifest(`${packageName}@latest`);
return manifest.version;
};
// 获取 create 当前版本
// Get create-stdf current version
const { version } = JSON.parse(fs.readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf-8'));
// 获取 create-stdf 的最新版本号
// Get the latest version number of create-stdf
const createStdfV = await getLatestVersion('create-stdf');
// 显示版本号
// Display version number
console.log(`
${grey(`create-stdf@${version}`)}
`);
if (version != createStdfV) {
console.log(
red(`😢 Recommended to use the latest version: ${createStdfV}
`)
);
}
const spinner = p.spinner();
p.intro('Welcome to use STDF!');
let lang = langAll.en_US;
// 获取命令行参数
// Get command line parameters
const argv = minimist(process.argv.slice(2));
// 获取项目名称和模板名称和语言
// Get project name and template name
const argvProjectName = argv._[0];
const argvTemplate = argv.template || argv.t;
const argvLanguage = argv.language || argv.l;
const argvIconUsage = argv.iconUsage || argv.i;
// 语言列表
// Language list
const languages = [];
// 循环 langAll 对象,将语言列表中的语言名字替换为对应的语言名字,且按照 sort 排序
// Loop through the langAll object and replace the language name in the language list with the corresponding language name, and sort by sort
for (const key in langAll) {
languages.push({ value: key, label: langAll[key].name, sort: langAll[key].sort });
}
// 按照 sort 排序
// Sort by sort
languages.sort((a, b) => a.sort - b.sort);
// 如果命令行参数中有语言,且语言列表中有该语言,使用该语言,否则使用英语
// If there is a language in the command line parameters and the language list has the language, use the language, otherwise use English
lang = argvLanguage && languages.find(item => item.value === argvLanguage) ? langAll[argvLanguage] : langAll.en_US;
// 模板列表
// Template list
const templateOptions = [
{ value: 'sktt', label: 'SvelteKit & Tailwind & TypeScript', template: 'sktt', finish: true },
{ value: 'skt', label: 'SvelteKit & Tailwind', template: 'skt', finish: true },
{ value: 'skut', label: 'SvelteKit & UnoCSS & TypeScript', template: 'skut', finish: false },
{ value: 'sku', label: 'SvelteKit & UnoCSS', template: 'sku', finish: false },
];
// 包管理工具列表
// Package management tool list
const packageManagerOptions = [
{ value: 'npm', label: 'NPM', install: 'npm i', dev: 'npm run dev' },
{ value: 'bun', label: 'Bun', install: 'bun i', dev: 'bun dev' },
{ value: 'pnpm', label: 'PNPM', install: 'pnpm i', dev: 'pnpm dev' },
{ value: 'yarn', label: 'Yarn', install: 'yarn', dev: 'yarn run dev' },
];
// 图标使用方式列表
// Icon usage method list
const iconUsageOptions = [
{ value: 'stdf-icon', label: 'rollup-plugin-stdf-icon' },
{ value: 'iconify', label: 'iconify' },
{ value: 'both', label: 'rollup-plugin-stdf-icon & iconify' },
{ value: 'none', label: 'none' },
];
// 如果命令行参数中有项目名称
// If there is project name in command line parameters
if (argvProjectName) {
let itemTemplate = null;
if (argvTemplate) {
itemTemplate = templateOptions.find(item => item.value === argvTemplate);
if (!itemTemplate) {
p.intro(red(lang.pectn + ' (' + templateOptions.map(item => item.value).join(', ') + ')'));
process.exit(0);
}
if (!itemTemplate.finish) {
p.intro(red(itemTemplate.label + ' ' + lang.hnay));
process.exit(0);
}
} else {
itemTemplate = templateOptions[0];
}
let itemIconUsage = null;
if (argvIconUsage) {
itemIconUsage = iconUsageOptions.find(item => item.value === argvIconUsage);
if (!itemIconUsage) {
p.intro(red(lang.pic + ' (' + iconUsageOptions.map(item => item.value).join(', ') + ')'));
process.exit(0);
}
} else {
itemIconUsage = iconUsageOptions[0];
}
// 判断是否已存在,提示"项目名称已存在"
// Determine whether it already exists, prompt "Project name already exists"
if (fs.existsSync(argvProjectName)) {
p.intro(red('🚫 ' + argvProjectName + ' ' + lang.pane));
process.exit(0);
}
createFunc(argvProjectName, itemTemplate, itemIconUsage, packageManagerOptions[0]);
} else {
(async () => {
// 选择一种语言
// Select a language
const languageType = await p.select({
message: bold('Please select your preferred language'),
options: languages,
});
if (p.isCancel(languageType)) {
p.cancel(red('⛔ ') + lang.oc);
process.exit(0);
}
lang = langAll[languageType];
// 选择一个模板
// Select a template
let template = await p.select({
message: bold(lang.psat),
options: templateOptions.map(item => ({
...item,
label: item.finish ? item.label : `(${lang.hnay}) ${item.label}`,
})),
});
if (p.isCancel(template)) {
p.cancel(red('⛔ ') + lang.oc);
process.exit(0);
}
// 直到选择的 template 所在项 的 finish 为 true 为止,否则一直重新选择
// Until the finish of the selected template is true, otherwise keep reselecting
while (!templateOptions.find(item => item.value === template)?.finish) {
if (p.isCancel(template)) {
p.cancel(red('⛔ ') + lang.oc);
process.exit(0);
}
p.intro(red(templateOptions.find(item => item.value === template).label + ' ' + lang.hnay + ' ' + lang.pca));
template = await p.select({
message: bold(lang.psat),
options: templateOptions.map(item => ({
...item,
label: item.finish ? item.label : `(${lang.hnay}) ${item.label}`,
})),
});
}
// 选择图标使用方式
// Select icon usage method
const iconUsage = await p.select({
message: bold(lang.psai),
options: iconUsageOptions,
});
if (p.isCancel(iconUsage)) {
p.cancel(red('⛔ ') + lang.oc);
process.exit(0);
}
// 输入项目名称
// Enter the project name
const projectName = await p.text({
message: bold(lang.pn),
placeholder: 'stdf-project',
validate: value => {
if (!value) {
// 判断是否为空,提示"项目名称不能为空"
// Determine whether it is empty, prompt "Project name cannot be empty"
return lang.pncbne;
}
if (fs.existsSync(value)) {
// 判断是否已存在,提示"项目名称已存在"
// Determine whether it already exists, prompt "Project name already exists"
return '🚫 ' + value + ' ' + lang.pane;
}
},
});
if (p.isCancel(projectName)) {
p.cancel(red('⛔ ') + lang.oc);
process.exit(0);
}
// 使用什么包管理工具 npm / pnpm / yarn / bun / deno
// What package management tool to use npm / pnpm / yarn / bun / deno
const packageManager = await p.select({
message: bold(lang.pm),
options: packageManagerOptions,
});
if (p.isCancel(packageManager)) {
p.cancel(red('⛔ ') + lang.oc);
process.exit(0);
}
// 根据 template 的值,复制对应目录下的所有文件到当前目录
// According to the value of template, copy all files under the corresponding directory to the current directory
createFunc(
projectName,
templateOptions.find(i => i.value === template),
iconUsageOptions.find(i => i.value === iconUsage),
packageManagerOptions.find(i => i.value === packageManager)
);
})();
}
function createFunc(projectName, templateItem, iconUsageItem, packageManagerItem) {
// 如果 projectName 是数字,转为字符串
// If projectName is a number, convert it to a string
if (typeof projectName === 'number') {
projectName = projectName.toString();
}
// 项目目录
// Project directory
const projectDir = path.join(path.resolve(), projectName);
spinner.start('🚀 ' + lang.cfsing);
fs.mkdirSync(projectDir);
// 获取模板目录的绝对路径,考虑到 Windows 系统的兼容性,使用 path.join
// Get the absolute path of the template directory, considering the compatibility of the Windows system, use path.join
const templatePath = path.resolve(fileURLToPath(import.meta.url), '../..', `templates/${templateItem.template}`);
// 将 templatePath 目录下的所有文件复制到 projectDir 目录下
// Copy all files under the templatePath directory to the projectDir directory
fs.copy(templatePath, projectDir)
.then(async () => {
// 读取 package.json 文件
// Read the package.json file
const packageJson = JSON.parse(fs.readFileSync(`${projectDir}/package.json`, 'utf-8'));
// 将项目内的 package.json 中的 name 属性修改为 projectName
// Modify the name attribute in package.json in the project to projectName
packageJson.name = projectName;
// 获取 stdf 的最新版本号
// Get the latest version number of stdf
const stdfV = await getLatestVersion('stdf');
packageJson.devDependencies['stdf'] = `^${stdfV}`;
const addIconifyFun = async () => {
const iconifyTailwind4V = await getLatestVersion('@iconify/tailwind4');
const bitcoin_iconsV = await getLatestVersion('@iconify-json/bitcoin-icons');
const duo_iconsV = await getLatestVersion('@iconify-json/duo-icons');
const fluent_colorV = await getLatestVersion('@iconify-json/fluent-color');
packageJson.devDependencies['@iconify/tailwind4'] = `^${iconifyTailwind4V}`;
packageJson.devDependencies['@iconify-json/bitcoin-icons'] = `^${bitcoin_iconsV}`;
packageJson.devDependencies['@iconify-json/duo-icons'] = `^${duo_iconsV}`;
packageJson.devDependencies['@iconify-json/fluent-color'] = `^${fluent_colorV}`;
// 在 ${projectDir}/src/app.css 的第 4 行增加 @plugin "@iconify/tailwind4" {
// prefixes: duo-icons, bitcoin-icons, fluent-color;
// }
const appCss = fs.readFileSync(`${projectDir}/src/app.css`, 'utf-8');
const appCssLines = appCss.split('\n');
appCssLines.splice(
3,
0,
`
@plugin "@iconify/tailwind4" {
prefixes: duo-icons, bitcoin-icons, fluent-color;
}`
);
fs.writeFileSync(`${projectDir}/src/app.css`, appCssLines.join('\n'), 'utf-8');
// 在 ${projectDir}/src/routes/+page.svelte 的 下方增加图标使用示例
const pageSvelte = fs.readFileSync(`${projectDir}/src/routes/+page.svelte`, 'utf-8');
const pageSvelteLines = pageSvelte.split('\n');
const iconifySnippet = fs.readFileSync(fileURLToPath(new URL('../snippet/iconify.txt', import.meta.url)), 'utf-8');
pageSvelteLines.splice(pageSvelteLines.indexOf('') + 1, 0, iconifySnippet);
fs.writeFileSync(`${projectDir}/src/routes/+page.svelte`, pageSvelteLines.join('\n'), 'utf-8');
};
const addStdfIconFun = async () => {
const isTs = templateItem.value.includes('tt') || templateItem.value.includes('ut');
const rollupPluginStdfIconV = await getLatestVersion('rollup-plugin-stdf-icon');
packageJson.devDependencies['rollup-plugin-stdf-icon'] = `^${rollupPluginStdfIconV}`;
const viteConfig = fs.readFileSync(`${projectDir}/vite.config.${isTs ? 'ts' : 'js'}`, 'utf-8');
const viteConfigLines = viteConfig.split('\n');
viteConfigLines.splice(1, 0, `import svgSprite from 'rollup-plugin-stdf-icon';`);
const viteStdfIconSnippet = fs.readFileSync(
fileURLToPath(new URL('../snippet/vite-stdf-icon.txt', import.meta.url)),
'utf-8'
);
// 将【export default defineConfig({ plugins: [tailwindcss(), sveltekit()] });】替换为 viteStdfIconSnippet 的代码
// Replace 【export default defineConfig({ plugins: [tailwindcss(), sveltekit()] });】 with the code of viteStdfIconSnippet
viteConfigLines.splice(
viteConfigLines.indexOf('export default defineConfig({ plugins: [tailwindcss(), sveltekit()] });'),
1,
viteStdfIconSnippet
);
fs.writeFileSync(`${projectDir}/vite.config.${isTs ? 'ts' : 'js'}`, viteConfigLines.join('\n'), 'utf-8');
// 将 snippet/svgs 整个目录复制到 ${projectDir}/src/lib 目录下
// Copy the snippet/svgs directory to the ${projectDir}/src/lib directory
fs.copySync(fileURLToPath(new URL('../snippet/svgs', import.meta.url)), `${projectDir}/src/lib/svgs`);
// 在 ${projectDir}/src/routes/+page.svelte 的 下方增加图标使用示例
const pageSvelte = fs.readFileSync(`${projectDir}/src/routes/+page.svelte`, 'utf-8');
const pageSvelteLines = pageSvelte.split('\n');
const stdfIconSnippet = fs.readFileSync(fileURLToPath(new URL('../snippet/stdf-icon.txt', import.meta.url)), 'utf-8');
pageSvelteLines.splice(pageSvelteLines.indexOf('') + 1, 0, stdfIconSnippet);
fs.writeFileSync(`${projectDir}/src/routes/+page.svelte`, pageSvelteLines.join('\n'), 'utf-8');
};
// 如果 iconUsageItem 的值为 iconify
// If the value of iconUsageItem is iconify
if (iconUsageItem.value === 'iconify') {
await addIconifyFun();
}
// 如果 iconUsageItem 的值为 stdf-icon,则获取 rollup-plugin-stdf-icon 的最新版本号
// If the value of iconUsageItem is stdf-icon, get the latest version number of rollup-plugin-stdf-icon
if (iconUsageItem.value === 'stdf-icon') {
await addStdfIconFun();
}
// 如果 iconUsageItem 的值为 both,则同时调用 addIconifyFun 和 addStdfIconFun
// If the value of iconUsageItem is both, call addIconifyFun and addStdfIconFun
if (iconUsageItem.value === 'both') {
await addIconifyFun();
await addStdfIconFun();
}
// 将修改后的 packageJson 写入到项目内的 package.json 文件中
// Write the modified packageJson to the package.json file in the project
fs.writeFileSync(`${projectDir}/package.json`, JSON.stringify(packageJson, null, 4), 'utf-8');
spinner.stop();
p.outro(`🎉🎉🎉 ${projectName} - ${lang.pcsucc}`);
// 根据 item.value 的值,判断使用 Tailwind 还是 UnoCSS
// According to the value of item.value, determine whether to use Tailwind or UnoCSS
const isHasUno = templateItem.value.includes('u');
// 获得依赖的版本号
// get the version number of the dependency
const versions = {
vite: packageJson.devDependencies.vite.replace('^', ''),
svelte: packageJson.devDependencies.svelte.replace('^', ''),
'@sveltejs/kit': packageJson.devDependencies['@sveltejs/kit'].replace('^', ''),
stdf: packageJson.devDependencies.stdf.replace('^', ''),
};
if (isHasUno) {
versions['unocss'] = packageJson.devDependencies.unocss.replace('^', '');
} else {
versions['tailwindcss'] = packageJson.devDependencies.tailwindcss.replace('^', '');
}
// 将 versions 的键值拼接为 bold('Vite:') cyan(versions.vite) bold('Svelte:') cyan(versions.svelte) 的形式
// Splice the key value of versions into the form of bold('Vite:') cyan(versions.vite) bold('Svelte:') cyan(versions.svelte)
let versionsString = '';
for (const key in versions) {
versionsString += bold(key) + ': ' + cyan(versions[key]) + ' ';
}
// 显示版本号
// Display version number
console.log(`📦 ${versionsString}
`);
// 显示提示信息
// Display prompt information
console.log(
`👉 ${bold(lang.tgs)}
${blue(`1. cd ${projectName}`)}
${blue(`2. git init && git add -A && git commit -m "Initial commit"`)}
${blue(`3. ${packageManagerItem.install}`)}
${blue(`4. ${packageManagerItem.dev}`)}
`
);
// 提示配置主题色
// Prompt configuration theme color
console.log(`🎨 ${grey(isHasUno ? lang.pcyt_vu : lang.pcyt_vt)}`);
})
.catch(err => {
spinner.stop();
console.error(red(lang.cferror + '--' + err));
});
}