index.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. #!/usr/bin/env node
  2. import fs from 'fs-extra';
  3. import path from 'node:path';
  4. import { fileURLToPath } from 'node:url';
  5. import * as p from '@clack/prompts';
  6. import { bold, cyan, grey, red, blue } from 'kleur/colors';
  7. import minimist from 'minimist';
  8. import pacote from 'pacote';
  9. import * as langAll from './lang';
  10. // 获取最新版本号
  11. // Get the latest version number
  12. const getLatestVersion = async packageName => {
  13. const manifest = await pacote.manifest(`${packageName}@latest`);
  14. return manifest.version;
  15. };
  16. // 获取 create 当前版本
  17. // Get create-stdf current version
  18. const { version } = JSON.parse(fs.readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf-8'));
  19. // 获取 create-stdf 的最新版本号
  20. // Get the latest version number of create-stdf
  21. const createStdfV = await getLatestVersion('create-stdf');
  22. // 显示版本号
  23. // Display version number
  24. console.log(`
  25. ${grey(`create-stdf@${version}`)}
  26. `);
  27. if (version != createStdfV) {
  28. console.log(
  29. red(`😢 Recommended to use the latest version: ${createStdfV}
  30. `)
  31. );
  32. }
  33. const spinner = p.spinner();
  34. p.intro('Welcome to use STDF!');
  35. let lang = langAll.en_US;
  36. // 获取命令行参数
  37. // Get command line parameters
  38. const argv = minimist(process.argv.slice(2));
  39. // 获取项目名称和模板名称和语言
  40. // Get project name and template name
  41. const argvProjectName = argv._[0];
  42. const argvTemplate = argv.template || argv.t;
  43. const argvLanguage = argv.language || argv.l;
  44. const argvIconUsage = argv.iconUsage || argv.i;
  45. // 语言列表
  46. // Language list
  47. const languages = [];
  48. // 循环 langAll 对象,将语言列表中的语言名字替换为对应的语言名字,且按照 sort 排序
  49. // Loop through the langAll object and replace the language name in the language list with the corresponding language name, and sort by sort
  50. for (const key in langAll) {
  51. languages.push({ value: key, label: langAll[key].name, sort: langAll[key].sort });
  52. }
  53. // 按照 sort 排序
  54. // Sort by sort
  55. languages.sort((a, b) => a.sort - b.sort);
  56. // 如果命令行参数中有语言,且语言列表中有该语言,使用该语言,否则使用英语
  57. // If there is a language in the command line parameters and the language list has the language, use the language, otherwise use English
  58. lang = argvLanguage && languages.find(item => item.value === argvLanguage) ? langAll[argvLanguage] : langAll.en_US;
  59. // 模板列表
  60. // Template list
  61. const templateOptions = [
  62. { value: 'sktt', label: 'SvelteKit & Tailwind & TypeScript', template: 'sktt', finish: true },
  63. { value: 'skt', label: 'SvelteKit & Tailwind', template: 'skt', finish: true },
  64. { value: 'skut', label: 'SvelteKit & UnoCSS & TypeScript', template: 'skut', finish: false },
  65. { value: 'sku', label: 'SvelteKit & UnoCSS', template: 'sku', finish: false },
  66. ];
  67. // 包管理工具列表
  68. // Package management tool list
  69. const packageManagerOptions = [
  70. { value: 'npm', label: 'NPM', install: 'npm i', dev: 'npm run dev' },
  71. { value: 'bun', label: 'Bun', install: 'bun i', dev: 'bun dev' },
  72. { value: 'pnpm', label: 'PNPM', install: 'pnpm i', dev: 'pnpm dev' },
  73. { value: 'yarn', label: 'Yarn', install: 'yarn', dev: 'yarn run dev' },
  74. ];
  75. // 图标使用方式列表
  76. // Icon usage method list
  77. const iconUsageOptions = [
  78. { value: 'stdf-icon', label: 'rollup-plugin-stdf-icon' },
  79. { value: 'iconify', label: 'iconify' },
  80. { value: 'both', label: 'rollup-plugin-stdf-icon & iconify' },
  81. { value: 'none', label: 'none' },
  82. ];
  83. // 如果命令行参数中有项目名称
  84. // If there is project name in command line parameters
  85. if (argvProjectName) {
  86. let itemTemplate = null;
  87. if (argvTemplate) {
  88. itemTemplate = templateOptions.find(item => item.value === argvTemplate);
  89. if (!itemTemplate) {
  90. p.intro(red(lang.pectn + ' (' + templateOptions.map(item => item.value).join(', ') + ')'));
  91. process.exit(0);
  92. }
  93. if (!itemTemplate.finish) {
  94. p.intro(red(itemTemplate.label + ' ' + lang.hnay));
  95. process.exit(0);
  96. }
  97. } else {
  98. itemTemplate = templateOptions[0];
  99. }
  100. let itemIconUsage = null;
  101. if (argvIconUsage) {
  102. itemIconUsage = iconUsageOptions.find(item => item.value === argvIconUsage);
  103. if (!itemIconUsage) {
  104. p.intro(red(lang.pic + ' (' + iconUsageOptions.map(item => item.value).join(', ') + ')'));
  105. process.exit(0);
  106. }
  107. } else {
  108. itemIconUsage = iconUsageOptions[0];
  109. }
  110. // 判断是否已存在,提示"项目名称已存在"
  111. // Determine whether it already exists, prompt "Project name already exists"
  112. if (fs.existsSync(argvProjectName)) {
  113. p.intro(red('🚫 ' + argvProjectName + ' ' + lang.pane));
  114. process.exit(0);
  115. }
  116. createFunc(argvProjectName, itemTemplate, itemIconUsage, packageManagerOptions[0]);
  117. } else {
  118. (async () => {
  119. // 选择一种语言
  120. // Select a language
  121. const languageType = await p.select({
  122. message: bold('Please select your preferred language'),
  123. options: languages,
  124. });
  125. if (p.isCancel(languageType)) {
  126. p.cancel(red('⛔ ') + lang.oc);
  127. process.exit(0);
  128. }
  129. lang = langAll[languageType];
  130. // 选择一个模板
  131. // Select a template
  132. let template = await p.select({
  133. message: bold(lang.psat),
  134. options: templateOptions.map(item => ({
  135. ...item,
  136. label: item.finish ? item.label : `(${lang.hnay}) ${item.label}`,
  137. })),
  138. });
  139. if (p.isCancel(template)) {
  140. p.cancel(red('⛔ ') + lang.oc);
  141. process.exit(0);
  142. }
  143. // 直到选择的 template 所在项 的 finish 为 true 为止,否则一直重新选择
  144. // Until the finish of the selected template is true, otherwise keep reselecting
  145. while (!templateOptions.find(item => item.value === template)?.finish) {
  146. if (p.isCancel(template)) {
  147. p.cancel(red('⛔ ') + lang.oc);
  148. process.exit(0);
  149. }
  150. p.intro(red(templateOptions.find(item => item.value === template).label + ' ' + lang.hnay + ' ' + lang.pca));
  151. template = await p.select({
  152. message: bold(lang.psat),
  153. options: templateOptions.map(item => ({
  154. ...item,
  155. label: item.finish ? item.label : `(${lang.hnay}) ${item.label}`,
  156. })),
  157. });
  158. }
  159. // 选择图标使用方式
  160. // Select icon usage method
  161. const iconUsage = await p.select({
  162. message: bold(lang.psai),
  163. options: iconUsageOptions,
  164. });
  165. if (p.isCancel(iconUsage)) {
  166. p.cancel(red('⛔ ') + lang.oc);
  167. process.exit(0);
  168. }
  169. // 输入项目名称
  170. // Enter the project name
  171. const projectName = await p.text({
  172. message: bold(lang.pn),
  173. placeholder: 'stdf-project',
  174. validate: value => {
  175. if (!value) {
  176. // 判断是否为空,提示"项目名称不能为空"
  177. // Determine whether it is empty, prompt "Project name cannot be empty"
  178. return lang.pncbne;
  179. }
  180. if (fs.existsSync(value)) {
  181. // 判断是否已存在,提示"项目名称已存在"
  182. // Determine whether it already exists, prompt "Project name already exists"
  183. return '🚫 ' + value + ' ' + lang.pane;
  184. }
  185. },
  186. });
  187. if (p.isCancel(projectName)) {
  188. p.cancel(red('⛔ ') + lang.oc);
  189. process.exit(0);
  190. }
  191. // 使用什么包管理工具 npm / pnpm / yarn / bun / deno
  192. // What package management tool to use npm / pnpm / yarn / bun / deno
  193. const packageManager = await p.select({
  194. message: bold(lang.pm),
  195. options: packageManagerOptions,
  196. });
  197. if (p.isCancel(packageManager)) {
  198. p.cancel(red('⛔ ') + lang.oc);
  199. process.exit(0);
  200. }
  201. // 根据 template 的值,复制对应目录下的所有文件到当前目录
  202. // According to the value of template, copy all files under the corresponding directory to the current directory
  203. createFunc(
  204. projectName,
  205. templateOptions.find(i => i.value === template),
  206. iconUsageOptions.find(i => i.value === iconUsage),
  207. packageManagerOptions.find(i => i.value === packageManager)
  208. );
  209. })();
  210. }
  211. function createFunc(projectName, templateItem, iconUsageItem, packageManagerItem) {
  212. // 如果 projectName 是数字,转为字符串
  213. // If projectName is a number, convert it to a string
  214. if (typeof projectName === 'number') {
  215. projectName = projectName.toString();
  216. }
  217. // 项目目录
  218. // Project directory
  219. const projectDir = path.join(path.resolve(), projectName);
  220. spinner.start('🚀 ' + lang.cfsing);
  221. fs.mkdirSync(projectDir);
  222. // 获取模板目录的绝对路径,考虑到 Windows 系统的兼容性,使用 path.join
  223. // Get the absolute path of the template directory, considering the compatibility of the Windows system, use path.join
  224. const templatePath = path.resolve(fileURLToPath(import.meta.url), '../..', `templates/${templateItem.template}`);
  225. // 将 templatePath 目录下的所有文件复制到 projectDir 目录下
  226. // Copy all files under the templatePath directory to the projectDir directory
  227. fs.copy(templatePath, projectDir)
  228. .then(async () => {
  229. // 读取 package.json 文件
  230. // Read the package.json file
  231. const packageJson = JSON.parse(fs.readFileSync(`${projectDir}/package.json`, 'utf-8'));
  232. // 将项目内的 package.json 中的 name 属性修改为 projectName
  233. // Modify the name attribute in package.json in the project to projectName
  234. packageJson.name = projectName;
  235. // 获取 stdf 的最新版本号
  236. // Get the latest version number of stdf
  237. const stdfV = await getLatestVersion('stdf');
  238. packageJson.devDependencies['stdf'] = `^${stdfV}`;
  239. const addIconifyFun = async () => {
  240. const iconifyTailwind4V = await getLatestVersion('@iconify/tailwind4');
  241. const bitcoin_iconsV = await getLatestVersion('@iconify-json/bitcoin-icons');
  242. const duo_iconsV = await getLatestVersion('@iconify-json/duo-icons');
  243. const fluent_colorV = await getLatestVersion('@iconify-json/fluent-color');
  244. packageJson.devDependencies['@iconify/tailwind4'] = `^${iconifyTailwind4V}`;
  245. packageJson.devDependencies['@iconify-json/bitcoin-icons'] = `^${bitcoin_iconsV}`;
  246. packageJson.devDependencies['@iconify-json/duo-icons'] = `^${duo_iconsV}`;
  247. packageJson.devDependencies['@iconify-json/fluent-color'] = `^${fluent_colorV}`;
  248. // 在 ${projectDir}/src/app.css 的第 4 行增加 @plugin "@iconify/tailwind4" {
  249. // prefixes: duo-icons, bitcoin-icons, fluent-color;
  250. // }
  251. const appCss = fs.readFileSync(`${projectDir}/src/app.css`, 'utf-8');
  252. const appCssLines = appCss.split('\n');
  253. appCssLines.splice(
  254. 3,
  255. 0,
  256. `
  257. @plugin "@iconify/tailwind4" {
  258. prefixes: duo-icons, bitcoin-icons, fluent-color;
  259. }`
  260. );
  261. fs.writeFileSync(`${projectDir}/src/app.css`, appCssLines.join('\n'), 'utf-8');
  262. // 在 ${projectDir}/src/routes/+page.svelte 的 <Calendar bind:visible /> 下方增加图标使用示例
  263. const pageSvelte = fs.readFileSync(`${projectDir}/src/routes/+page.svelte`, 'utf-8');
  264. const pageSvelteLines = pageSvelte.split('\n');
  265. const iconifySnippet = fs.readFileSync(fileURLToPath(new URL('../snippet/iconify.txt', import.meta.url)), 'utf-8');
  266. pageSvelteLines.splice(pageSvelteLines.indexOf('<Calendar bind:visible />') + 1, 0, iconifySnippet);
  267. fs.writeFileSync(`${projectDir}/src/routes/+page.svelte`, pageSvelteLines.join('\n'), 'utf-8');
  268. };
  269. const addStdfIconFun = async () => {
  270. const isTs = templateItem.value.includes('tt') || templateItem.value.includes('ut');
  271. const rollupPluginStdfIconV = await getLatestVersion('rollup-plugin-stdf-icon');
  272. packageJson.devDependencies['rollup-plugin-stdf-icon'] = `^${rollupPluginStdfIconV}`;
  273. const viteConfig = fs.readFileSync(`${projectDir}/vite.config.${isTs ? 'ts' : 'js'}`, 'utf-8');
  274. const viteConfigLines = viteConfig.split('\n');
  275. viteConfigLines.splice(1, 0, `import svgSprite from 'rollup-plugin-stdf-icon';`);
  276. const viteStdfIconSnippet = fs.readFileSync(
  277. fileURLToPath(new URL('../snippet/vite-stdf-icon.txt', import.meta.url)),
  278. 'utf-8'
  279. );
  280. // 将【export default defineConfig({ plugins: [tailwindcss(), sveltekit()] });】替换为 viteStdfIconSnippet 的代码
  281. // Replace 【export default defineConfig({ plugins: [tailwindcss(), sveltekit()] });】 with the code of viteStdfIconSnippet
  282. viteConfigLines.splice(
  283. viteConfigLines.indexOf('export default defineConfig({ plugins: [tailwindcss(), sveltekit()] });'),
  284. 1,
  285. viteStdfIconSnippet
  286. );
  287. fs.writeFileSync(`${projectDir}/vite.config.${isTs ? 'ts' : 'js'}`, viteConfigLines.join('\n'), 'utf-8');
  288. // 将 snippet/svgs 整个目录复制到 ${projectDir}/src/lib 目录下
  289. // Copy the snippet/svgs directory to the ${projectDir}/src/lib directory
  290. fs.copySync(fileURLToPath(new URL('../snippet/svgs', import.meta.url)), `${projectDir}/src/lib/svgs`);
  291. // 在 ${projectDir}/src/routes/+page.svelte 的 <Calendar bind:visible /> 下方增加图标使用示例
  292. const pageSvelte = fs.readFileSync(`${projectDir}/src/routes/+page.svelte`, 'utf-8');
  293. const pageSvelteLines = pageSvelte.split('\n');
  294. const stdfIconSnippet = fs.readFileSync(fileURLToPath(new URL('../snippet/stdf-icon.txt', import.meta.url)), 'utf-8');
  295. pageSvelteLines.splice(pageSvelteLines.indexOf('<Calendar bind:visible />') + 1, 0, stdfIconSnippet);
  296. fs.writeFileSync(`${projectDir}/src/routes/+page.svelte`, pageSvelteLines.join('\n'), 'utf-8');
  297. };
  298. // 如果 iconUsageItem 的值为 iconify
  299. // If the value of iconUsageItem is iconify
  300. if (iconUsageItem.value === 'iconify') {
  301. await addIconifyFun();
  302. }
  303. // 如果 iconUsageItem 的值为 stdf-icon,则获取 rollup-plugin-stdf-icon 的最新版本号
  304. // If the value of iconUsageItem is stdf-icon, get the latest version number of rollup-plugin-stdf-icon
  305. if (iconUsageItem.value === 'stdf-icon') {
  306. await addStdfIconFun();
  307. }
  308. // 如果 iconUsageItem 的值为 both,则同时调用 addIconifyFun 和 addStdfIconFun
  309. // If the value of iconUsageItem is both, call addIconifyFun and addStdfIconFun
  310. if (iconUsageItem.value === 'both') {
  311. await addIconifyFun();
  312. await addStdfIconFun();
  313. }
  314. // 将修改后的 packageJson 写入到项目内的 package.json 文件中
  315. // Write the modified packageJson to the package.json file in the project
  316. fs.writeFileSync(`${projectDir}/package.json`, JSON.stringify(packageJson, null, 4), 'utf-8');
  317. spinner.stop();
  318. p.outro(`🎉🎉🎉 ${projectName} - ${lang.pcsucc}`);
  319. // 根据 item.value 的值,判断使用 Tailwind 还是 UnoCSS
  320. // According to the value of item.value, determine whether to use Tailwind or UnoCSS
  321. const isHasUno = templateItem.value.includes('u');
  322. // 获得依赖的版本号
  323. // get the version number of the dependency
  324. const versions = {
  325. vite: packageJson.devDependencies.vite.replace('^', ''),
  326. svelte: packageJson.devDependencies.svelte.replace('^', ''),
  327. '@sveltejs/kit': packageJson.devDependencies['@sveltejs/kit'].replace('^', ''),
  328. stdf: packageJson.devDependencies.stdf.replace('^', ''),
  329. };
  330. if (isHasUno) {
  331. versions['unocss'] = packageJson.devDependencies.unocss.replace('^', '');
  332. } else {
  333. versions['tailwindcss'] = packageJson.devDependencies.tailwindcss.replace('^', '');
  334. }
  335. // 将 versions 的键值拼接为 bold('Vite:') cyan(versions.vite) bold('Svelte:') cyan(versions.svelte) 的形式
  336. // Splice the key value of versions into the form of bold('Vite:') cyan(versions.vite) bold('Svelte:') cyan(versions.svelte)
  337. let versionsString = '';
  338. for (const key in versions) {
  339. versionsString += bold(key) + ': ' + cyan(versions[key]) + ' ';
  340. }
  341. // 显示版本号
  342. // Display version number
  343. console.log(`📦 ${versionsString}
  344. `);
  345. // 显示提示信息
  346. // Display prompt information
  347. console.log(
  348. `👉 ${bold(lang.tgs)}
  349. ${blue(`1. cd ${projectName}`)}
  350. ${blue(`2. git init && git add -A && git commit -m "Initial commit"`)}
  351. ${blue(`3. ${packageManagerItem.install}`)}
  352. ${blue(`4. ${packageManagerItem.dev}`)}
  353. `
  354. );
  355. // 提示配置主题色
  356. // Prompt configuration theme color
  357. console.log(`🎨 ${grey(isHasUno ? lang.pcyt_vu : lang.pcyt_vt)}`);
  358. })
  359. .catch(err => {
  360. spinner.stop();
  361. console.error(red(lang.cferror + '--' + err));
  362. });
  363. }