#!/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)); }); }