create-rasti 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +96 -0
  3. package/bin/create-rasti.js +4 -0
  4. package/extras/cn/README.md +57 -0
  5. package/extras/cn/package.json +9 -0
  6. package/extras/cn/src/index.js +37 -0
  7. package/extras/micro-router/README.md +78 -0
  8. package/extras/micro-router/package-lock.json +26 -0
  9. package/extras/micro-router/package.json +12 -0
  10. package/extras/micro-router/src/index.js +192 -0
  11. package/extras/rasti-icons/README.md +65 -0
  12. package/extras/rasti-icons/bin/rasti-icons.js +84 -0
  13. package/extras/rasti-icons/package.json +11 -0
  14. package/extras/rasti-icons/src/generate.js +119 -0
  15. package/extras/rasti-icons/src/presets.js +57 -0
  16. package/package.json +54 -0
  17. package/src/apply/base.js +29 -0
  18. package/src/apply/cssfun.js +38 -0
  19. package/src/apply/description.js +56 -0
  20. package/src/apply/featuresInclude.js +75 -0
  21. package/src/apply/icons.js +21 -0
  22. package/src/apply/index.js +134 -0
  23. package/src/apply/router.js +50 -0
  24. package/src/apply/ssr.js +29 -0
  25. package/src/apply/static.js +46 -0
  26. package/src/apply/tailwind.js +33 -0
  27. package/src/args.js +55 -0
  28. package/src/cli.js +91 -0
  29. package/src/plan.js +33 -0
  30. package/src/prompts.js +116 -0
  31. package/src/utils/copy.js +21 -0
  32. package/src/utils/exec.js +83 -0
  33. package/src/utils/logger.js +79 -0
  34. package/src/utils/pkg.js +87 -0
  35. package/src/utils/template.js +205 -0
  36. package/src/validate.js +48 -0
  37. package/src/versions.js +17 -0
  38. package/templates/AGENTS.md +48 -0
  39. package/templates/_base/App-cssfun.js +88 -0
  40. package/templates/_base/App-tailwind.js +58 -0
  41. package/templates/_base/App.js +58 -0
  42. package/templates/_base/components/Button-cssfun.js +51 -0
  43. package/templates/_base/components/Button-tailwind.js +52 -0
  44. package/templates/_base/components/Button.js +22 -0
  45. package/templates/_base/components/Header-cssfun.js +69 -0
  46. package/templates/_base/components/Header-tailwind.js +17 -0
  47. package/templates/_base/components/Header.js +17 -0
  48. package/templates/_base/components/Home-cssfun.js +98 -0
  49. package/templates/_base/components/Home-tailwind.js +35 -0
  50. package/templates/_base/components/Home.js +35 -0
  51. package/templates/_base/style.css +170 -0
  52. package/templates/_extras/router/components/About-cssfun.js +43 -0
  53. package/templates/_extras/router/components/About-tailwind.js +14 -0
  54. package/templates/_extras/router/components/About.js +16 -0
  55. package/templates/_extras/router/router-setup.js +60 -0
  56. package/templates/_features/cssfun/index.html +14 -0
  57. package/templates/_features/cssfun/theme.js +60 -0
  58. package/templates/_features/tailwind/style.css +26 -0
  59. package/templates/_features/tailwind/vite.config.js +8 -0
  60. package/templates/spa/index.html +14 -0
  61. package/templates/spa/package.json +17 -0
  62. package/templates/spa/public/.gitkeep +0 -0
  63. package/templates/spa/src/main.js +15 -0
  64. package/templates/spa/vite.config.js +6 -0
  65. package/templates/ssr/app.js +71 -0
  66. package/templates/ssr/index.html +16 -0
  67. package/templates/ssr/package.json +23 -0
  68. package/templates/ssr/public/.gitkeep +0 -0
  69. package/templates/ssr/server.js +7 -0
  70. package/templates/ssr/src/entry-client.js +15 -0
  71. package/templates/ssr/src/entry-server.js +49 -0
  72. package/templates/ssr/vite.config.js +6 -0
  73. package/templates/static/scripts/build-static.js +161 -0
  74. package/templates/static/scripts/serve-static.js +19 -0
  75. package/templates/static/static.config.js +14 -0
@@ -0,0 +1,134 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ import { applyBase } from './base.js';
4
+ import { applySsr } from './ssr.js';
5
+ import { applyStatic } from './static.js';
6
+ import { applyTailwind } from './tailwind.js';
7
+ import { applyCssfun } from './cssfun.js';
8
+ import { applyIcons } from './icons.js';
9
+ import { applyRouter } from './router.js';
10
+ import { applyFeaturesInclude } from './featuresInclude.js';
11
+ import { getBaseContext } from './description.js';
12
+ import { copyTemplateFile } from '../utils/template.js';
13
+ import { createSpinner, log } from '../utils/logger.js';
14
+
15
+ const RASTI_AGENTS_MD_URL = 'https://raw.githubusercontent.com/8tentaculos/rasti/master/docs/AGENTS.md';
16
+
17
+ /**
18
+ * Fetch Rasti AGENTS.md and write to project root as AGENTS-RASTI.md.
19
+ * @param {string} targetDir - Project root directory
20
+ */
21
+ async function fetchRastiAgentsMd(targetDir) {
22
+ const res = await fetch(RASTI_AGENTS_MD_URL);
23
+ if (!res.ok) throw new Error(`Failed to fetch AGENTS-RASTI.md: ${res.status}`);
24
+ const text = await res.text();
25
+ await fs.writeFile(path.join(targetDir, 'AGENTS-RASTI.md'), text, 'utf-8');
26
+ }
27
+
28
+ /**
29
+ * Apply a project plan by copying templates and applying features.
30
+ * @param {object} plan - Project plan object
31
+ */
32
+ export async function applyPlan(plan) {
33
+ const targetDir = path.resolve(process.cwd(), plan.name);
34
+ const spinner = createSpinner();
35
+
36
+ const ctx = {
37
+ plan,
38
+ targetDir,
39
+ spinner
40
+ };
41
+
42
+ try {
43
+ spinner.start('Creating project...');
44
+
45
+ if (plan.base === 'ssr' || plan.base === 'static') {
46
+ await applySsr(ctx);
47
+ if (plan.base === 'static') {
48
+ await applyStatic(ctx);
49
+ }
50
+ } else {
51
+ await applyBase(ctx);
52
+ }
53
+
54
+ spinner.stop('Project created');
55
+
56
+ if (plan.features.tailwind) {
57
+ spinner.start('Adding Tailwind CSS...');
58
+ await applyTailwind(ctx);
59
+ spinner.stop('Tailwind CSS added');
60
+ }
61
+
62
+ if (plan.features.cssfun) {
63
+ spinner.start('Adding CSSFUN...');
64
+ await applyCssfun(ctx);
65
+ spinner.stop('CSSFUN added');
66
+ }
67
+
68
+ if (plan.features.router) {
69
+ spinner.start('Adding micro-router...');
70
+ await applyRouter(ctx);
71
+ spinner.stop('micro-router added');
72
+ }
73
+
74
+ if (plan.features.icons) {
75
+ const iconLabel = plan.features.icons.join(', ');
76
+ spinner.start(`Adding rasti-icons (${iconLabel})...`);
77
+ await applyIcons(ctx);
78
+ spinner.stop('rasti-icons added');
79
+ }
80
+
81
+ spinner.start('Writing features list...');
82
+ await applyFeaturesInclude(ctx);
83
+ spinner.stop('Features list written');
84
+
85
+ spinner.start('Writing AGENTS.md...');
86
+ const agentsContext = {
87
+ ...getBaseContext(plan),
88
+ SSR : (plan.base === 'ssr' || plan.base === 'static') ? 'true' : '',
89
+ STATIC : plan.base === 'static' ? 'true' : '',
90
+ TAILWIND : plan.features.tailwind ? 'true' : '',
91
+ CSSFUN : plan.features.cssfun ? 'true' : '',
92
+ ROUTER : plan.features.router ? 'true' : '',
93
+ ICONS : plan.features.icons ? 'true' : '',
94
+ };
95
+ await copyTemplateFile('AGENTS.md', path.join(ctx.targetDir, 'AGENTS.md'), agentsContext);
96
+ spinner.stop('AGENTS.md written');
97
+
98
+ spinner.start('Adding Rasti AGENTS.md...');
99
+ try {
100
+ await fetchRastiAgentsMd(ctx.targetDir);
101
+ spinner.stop('Rasti AGENTS.md added');
102
+ } catch {
103
+ spinner.stop('Could not fetch Rasti AGENTS.md (offline?) — skipped');
104
+ }
105
+
106
+ logSummary(plan);
107
+
108
+ } catch (error) {
109
+ spinner.stop('Failed');
110
+ throw error;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Log a summary of what was created.
116
+ * @param {object} plan - Project plan object
117
+ */
118
+ function logSummary(plan) {
119
+ const features = [];
120
+
121
+ if (plan.features.tailwind) features.push('Tailwind CSS');
122
+ if (plan.features.cssfun) features.push('CSSFUN');
123
+ if (plan.features.icons) {
124
+ features.push(`rasti-icons (${plan.features.icons.join(', ')})`);
125
+ }
126
+ if (plan.features.router) features.push('micro-router');
127
+
128
+ const featureList = features.length > 0
129
+ ? `\n Features: ${features.join(', ')}`
130
+ : '';
131
+
132
+ const templateLabel = plan.base === 'static' ? 'Static (SSR + static build)' : plan.base.toUpperCase();
133
+ log.info(`Project: ${plan.name}\n Template: ${templateLabel}${featureList}`);
134
+ }
@@ -0,0 +1,50 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ import { copyTemplateFile, copyStyledTemplate, copyExtraFile } from '../utils/template.js';
4
+ import { addDependencies } from '../utils/pkg.js';
5
+ import { getBaseContext } from './description.js';
6
+ import { VERSIONS } from '../versions.js';
7
+
8
+ /**
9
+ * Build the template context for router-related files.
10
+ * @param {object} plan - Project plan
11
+ * @returns {object} Template placeholder values
12
+ */
13
+ function getRouterContext(plan) {
14
+ const routeAbout = plan.base === 'static' ? '/about/' : '/about';
15
+ return {
16
+ ...getBaseContext(plan),
17
+ ROUTE_ABOUT : routeAbout,
18
+ CSSFUN : plan.features.cssfun ? 'true' : '',
19
+ TAILWIND : plan.features.tailwind ? 'true' : '',
20
+ ROUTER : 'true'
21
+ };
22
+ }
23
+
24
+ export async function applyRouter(ctx) {
25
+ const { plan, targetDir } = ctx;
26
+ const context = getRouterContext(plan);
27
+
28
+ await addDependencies(targetDir, { dependencies : { 'path-to-regexp' : VERSIONS.pathToRegexp } });
29
+
30
+ await copyExtraFile('micro-router/src/index.js', path.join(targetDir, 'src', 'lib', 'router.js'));
31
+
32
+ await copyTemplateFile('_extras/router/router-setup.js', path.join(targetDir, 'src', 'router-setup.js'), context);
33
+
34
+ const componentsDir = path.join(targetDir, 'src', 'components');
35
+ await fs.mkdir(componentsDir, { recursive : true });
36
+ await copyStyledTemplate('_extras/router/components/About.js', path.join(componentsDir, 'About.js'), context, plan);
37
+
38
+ if (!plan.features.cssfun) {
39
+ const stylePath = path.join(targetDir, 'src', 'style.css');
40
+ try {
41
+ let css = await fs.readFile(stylePath, 'utf-8');
42
+ if (!css.includes('.nav')) {
43
+ css += '\n.nav { display: flex; gap: 1rem; }\n';
44
+ await fs.writeFile(stylePath, css, 'utf-8');
45
+ }
46
+ } catch {
47
+ // no style.css (e.g. tailwind replaces it)
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,29 @@
1
+ import path from 'node:path';
2
+ import { copyTemplateDir, copyTemplateFile, copyStyledTemplate, copyExtraFile } from '../utils/template.js';
3
+ import { updatePackageName } from '../utils/pkg.js';
4
+ import { getBaseContext } from './description.js';
5
+
6
+ /**
7
+ * Apply the SSR base template.
8
+ * @param {object} ctx - Context object with plan and targetDir
9
+ */
10
+ export async function applySsr(ctx) {
11
+ const { plan, targetDir } = ctx;
12
+ const routeAbout = plan.base === 'static' ? '/about/' : '/about';
13
+ const context = {
14
+ ...getBaseContext(plan),
15
+ CSSFUN : plan.features.cssfun ? 'true' : '',
16
+ TAILWIND : plan.features.tailwind ? 'true' : '',
17
+ ROUTER : plan.features.router ? 'true' : '',
18
+ ROUTE_ABOUT : routeAbout
19
+ };
20
+
21
+ await copyTemplateDir('ssr', targetDir, context);
22
+ await copyStyledTemplate('_base/App.js', path.join(targetDir, 'src', 'App.js'), context, plan);
23
+ await copyStyledTemplate('_base/components/Button.js', path.join(targetDir, 'src', 'components', 'Button.js'), context, plan);
24
+ await copyStyledTemplate('_base/components/Home.js', path.join(targetDir, 'src', 'components', 'Home.js'), context, plan);
25
+ await copyStyledTemplate('_base/components/Header.js', path.join(targetDir, 'src', 'components', 'Header.js'), context, plan);
26
+ await copyTemplateFile('_base/style.css', path.join(targetDir, 'src', 'style.css'), context);
27
+ await copyExtraFile('cn/src/index.js', path.join(targetDir, 'src', 'lib', 'cn.js'));
28
+ await updatePackageName(targetDir, plan.name);
29
+ }
@@ -0,0 +1,46 @@
1
+ import path from 'node:path';
2
+ import { copyTemplateFile } from '../utils/template.js';
3
+ import { mergeScripts } from '../utils/pkg.js';
4
+
5
+ /**
6
+ * Build STATIC_ROUTES placeholder value for static.config.js.
7
+ * @param {object} plan - Project plan
8
+ * @returns {string} Lines for the array (e.g. " '/',\n '/about/',\n")
9
+ */
10
+ function getStaticRoutes(plan) {
11
+ if (plan.features.router) {
12
+ return ' \'/\',\n \'/about/\',\n';
13
+ }
14
+ return ' \'/\',\n';
15
+ }
16
+
17
+ /**
18
+ * Apply static overlay: static.config.js, build script, package.json scripts.
19
+ * @param {object} ctx - Context with plan and targetDir
20
+ */
21
+ export async function applyStatic(ctx) {
22
+ const { plan, targetDir } = ctx;
23
+
24
+ const staticRoutes = getStaticRoutes(plan);
25
+ await copyTemplateFile(
26
+ 'static/static.config.js',
27
+ path.join(targetDir, 'static.config.js'),
28
+ { STATIC_ROUTES : staticRoutes }
29
+ );
30
+
31
+ await copyTemplateFile(
32
+ 'static/scripts/build-static.js',
33
+ path.join(targetDir, 'scripts', 'build-static.js'),
34
+ {}
35
+ );
36
+ await copyTemplateFile(
37
+ 'static/scripts/serve-static.js',
38
+ path.join(targetDir, 'scripts', 'serve-static.js'),
39
+ {}
40
+ );
41
+
42
+ await mergeScripts(targetDir, {
43
+ 'build:static' : 'npm run build && node scripts/build-static.js',
44
+ 'preview:static' : 'node scripts/serve-static.js'
45
+ });
46
+ }
@@ -0,0 +1,33 @@
1
+ import path from 'node:path';
2
+ import { copyTemplateFile } from '../utils/template.js';
3
+ import { addDependencies } from '../utils/pkg.js';
4
+ import { VERSIONS } from '../versions.js';
5
+
6
+ /**
7
+ * Apply Tailwind CSS to the project.
8
+ * @param {object} ctx - Context object with plan and targetDir
9
+ */
10
+ export async function applyTailwind(ctx) {
11
+ const { targetDir } = ctx;
12
+
13
+ await addDependencies(targetDir, {
14
+ devDependencies : {
15
+ '@tailwindcss/vite' : VERSIONS.tailwindVite,
16
+ 'tailwindcss' : VERSIONS.tailwindcss
17
+ }
18
+ });
19
+
20
+ // Copy Tailwind style.css (overwrites base style.css)
21
+ await copyTemplateFile(
22
+ '_features/tailwind/style.css',
23
+ path.join(targetDir, 'src', 'style.css'),
24
+ {}
25
+ );
26
+
27
+ // Copy Tailwind vite.config.js (overwrites base vite.config.js)
28
+ await copyTemplateFile(
29
+ '_features/tailwind/vite.config.js',
30
+ path.join(targetDir, 'vite.config.js'),
31
+ {}
32
+ );
33
+ }
package/src/args.js ADDED
@@ -0,0 +1,55 @@
1
+ import { DEFAULT_PRESET } from '../extras/rasti-icons/src/presets.js';
2
+
3
+ /**
4
+ * Parse command line arguments without external dependencies.
5
+ * Supports flags (--flag) and positional arguments.
6
+ * @param {string[]} argv - Command line arguments (process.argv.slice(2))
7
+ * @returns {object} Parsed arguments object
8
+ */
9
+ export function parseArgs(argv) {
10
+ const args = {
11
+ name : null,
12
+ ssr : false,
13
+ static : false,
14
+ tailwind : false,
15
+ cssfun : false,
16
+ icons : null,
17
+ router : false,
18
+ help : false
19
+ };
20
+
21
+ for (let i = 0; i < argv.length; i++) {
22
+ const arg = argv[i];
23
+
24
+ if (arg === '--help' || arg === '-h') {
25
+ args.help = true;
26
+ } else if (arg === '--ssr') {
27
+ args.ssr = true;
28
+ } else if (arg === '--static') {
29
+ args.static = true;
30
+ } else if (arg === '--tailwind') {
31
+ args.tailwind = true;
32
+ } else if (arg === '--cssfun') {
33
+ args.cssfun = true;
34
+ } else if (arg === '--icons') {
35
+ if (!args.icons) args.icons = [];
36
+ const next = argv[i + 1];
37
+ if (next && !next.startsWith('-')) {
38
+ const parts = next.split(',').map(s => s.trim()).filter(Boolean);
39
+ args.icons.push(...parts);
40
+ i++;
41
+ } else {
42
+ args.icons.push(DEFAULT_PRESET);
43
+ }
44
+ } else if (arg === '--router') {
45
+ args.router = true;
46
+ } else if (!arg.startsWith('-')) {
47
+ // Positional argument - project name
48
+ if (!args.name) {
49
+ args.name = arg;
50
+ }
51
+ }
52
+ }
53
+
54
+ return args;
55
+ }
package/src/cli.js ADDED
@@ -0,0 +1,91 @@
1
+ import { parseArgs } from './args.js';
2
+ import { runPrompts } from './prompts.js';
3
+ import { createPlan } from './plan.js';
4
+ import { validatePlan } from './validate.js';
5
+ import { applyPlan } from './apply/index.js';
6
+ import { log, intro, outro } from './utils/logger.js';
7
+ import { DEFAULT_PRESET, PRESET_IDS } from '../extras/rasti-icons/src/presets.js';
8
+
9
+ /**
10
+ * Main entry point for the CLI.
11
+ * Handles both flag-based and interactive modes.
12
+ * @param {string[]} argv - Command line arguments
13
+ */
14
+ export async function run(argv) {
15
+ try {
16
+ intro();
17
+
18
+ const args = parseArgs(argv);
19
+
20
+ if (args.help) {
21
+ showHelp();
22
+ return;
23
+ }
24
+
25
+ const needsPrompts = !args.name;
26
+
27
+ let options;
28
+ if (needsPrompts) {
29
+ options = await runPrompts(args);
30
+ if (!options) return; // User cancelled
31
+ } else {
32
+ options = args;
33
+ }
34
+
35
+ const plan = createPlan(options);
36
+ const validation = validatePlan(plan);
37
+
38
+ if (!validation.valid) {
39
+ log.error(validation.error);
40
+ process.exit(1);
41
+ }
42
+
43
+ await applyPlan(plan);
44
+
45
+ outro(plan.name, plan.packageManager);
46
+ } catch (error) {
47
+ log.error(error.message);
48
+ process.exit(1);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Show help message.
54
+ */
55
+ function showHelp() {
56
+ const presetList = PRESET_IDS.join(', ');
57
+
58
+ console.log(`
59
+ create-rasti - Create Rasti + Vite projects
60
+
61
+ Usage:
62
+ npm create rasti [project-name] [options]
63
+
64
+ Options:
65
+ --ssr Use SSR template instead of SPA
66
+ --static Use Static template (SSR + pre-rendered static build)
67
+ --tailwind Add Tailwind CSS
68
+ --cssfun Add CSSFUN (CSS-in-JS with light and dark themes)
69
+ --icons [sets] Add rasti-icons (preset ids: ${DEFAULT_PRESET} (default), ${presetList}).
70
+ Repeat flag or use comma-separated list. Multiple sets are written under src/icons/<preset>/ each;
71
+ a single set uses src/icons/ directly.
72
+ --router Add micro-router (universal router, two pages and links)
73
+ --help Show this help message
74
+
75
+ Examples:
76
+ npm create rasti # Interactive mode
77
+ npm create rasti my-app # Create SPA project
78
+ npm create rasti my-app --ssr # Create SSR project
79
+ npm create rasti my-app --static # Create static site (SSR + static build)
80
+ npm create rasti my-app --tailwind # Create SPA with Tailwind
81
+ npm create rasti my-app --icons # SPA with ${DEFAULT_PRESET} in src/icons
82
+ npm create rasti my-app --icons heroicons-outline # SPA with heroicons-outline in src/icons
83
+ npm create rasti my-app --icons akar-icons # SPA with akar-icons in src/icons
84
+ npm create rasti my-app --icons feathericon # SPA with feathericon in src/icons
85
+ npm create rasti my-app --icons pixelarticons # SPA with pixelarticons in src/icons
86
+ npm create rasti my-app --icons heroicons-solid,heroicons-outline # Multiple sets in subfolders under src/icons
87
+ npm create rasti my-app --router # SPA with two pages and universal router
88
+
89
+ Note: --tailwind and --cssfun are mutually exclusive.
90
+ `);
91
+ }
package/src/plan.js ADDED
@@ -0,0 +1,33 @@
1
+ import { detectPackageManager } from './utils/exec.js';
2
+
3
+ /**
4
+ * Normalize icons option to false or a deduped preset id array.
5
+ * @param {string|string[]|false|null|undefined} icons
6
+ * @returns {false|string[]}
7
+ */
8
+ function normalizeIconSets(icons) {
9
+ if (icons == null || icons === false) return false;
10
+ const arr = Array.isArray(icons) ? icons : [icons];
11
+ const deduped = [...new Set(arr)];
12
+ return deduped.length ? deduped : false;
13
+ }
14
+
15
+ /**
16
+ * Create a project plan from user options.
17
+ * @param {object} options - User options from args or prompts
18
+ * @returns {object} Project plan object
19
+ */
20
+ export function createPlan(options) {
21
+ const base = options.static ? 'static' : (options.ssr ? 'ssr' : 'spa');
22
+ return {
23
+ name : options.name,
24
+ base,
25
+ features : {
26
+ tailwind : options.tailwind || false,
27
+ cssfun : options.cssfun || false,
28
+ icons : normalizeIconSets(options.icons),
29
+ router : options.router || false
30
+ },
31
+ packageManager : detectPackageManager()
32
+ };
33
+ }
package/src/prompts.js ADDED
@@ -0,0 +1,116 @@
1
+ import * as p from '@clack/prompts';
2
+ import { DEFAULT_PRESET, PRESET_OPTIONS } from '../extras/rasti-icons/src/presets.js';
3
+
4
+ /**
5
+ * Run interactive prompts to gather project options.
6
+ * @param {object} defaults - Default values from command line args
7
+ * @returns {Promise<object|null>} Options object or null if cancelled
8
+ */
9
+ export async function runPrompts(defaults = {}) {
10
+ const options = { ...defaults };
11
+
12
+ // Project name
13
+ const name = await p.text({
14
+ message : 'Project name',
15
+ placeholder : 'my-rasti-app',
16
+ validate : (value) => {
17
+ if (!value) return 'Project name is required';
18
+ if (!/^[a-z0-9-_]+$/i.test(value)) {
19
+ return 'Project name can only contain letters, numbers, dashes and underscores';
20
+ }
21
+ }
22
+ });
23
+
24
+ if (p.isCancel(name)) {
25
+ p.cancel('Operation cancelled.');
26
+ return null;
27
+ }
28
+
29
+ options.name = name;
30
+
31
+ // Template base
32
+ const base = await p.select({
33
+ message : 'Select template',
34
+ options : [
35
+ { value : 'spa', label : 'SPA', hint : 'Single Page Application' },
36
+ { value : 'ssr', label : 'SSR', hint : 'Server-Side Rendering' },
37
+ { value : 'static', label : 'Static', hint : 'Pre-rendered static site (SSR + static build)' }
38
+ ],
39
+ initialValue : defaults.static ? 'static' : (defaults.ssr ? 'ssr' : 'spa')
40
+ });
41
+
42
+ if (p.isCancel(base)) {
43
+ p.cancel('Operation cancelled.');
44
+ return null;
45
+ }
46
+
47
+ options.ssr = base === 'ssr' || base === 'static';
48
+ options.static = base === 'static';
49
+
50
+ // Styling option
51
+ const styling = await p.select({
52
+ message : 'Select styling',
53
+ options : [
54
+ { value : 'none', label : 'None', hint : 'Plain CSS' },
55
+ { value : 'tailwind', label : 'Tailwind CSS', hint : 'Utility-first CSS' },
56
+ { value : 'cssfun', label : 'CSSFUN', hint : 'CSS-in-JS with light and dark themes' }
57
+ ],
58
+ initialValue : defaults.tailwind ? 'tailwind' : defaults.cssfun ? 'cssfun' : 'none'
59
+ });
60
+
61
+ if (p.isCancel(styling)) {
62
+ p.cancel('Operation cancelled.');
63
+ return null;
64
+ }
65
+
66
+ options.tailwind = styling === 'tailwind';
67
+ options.cssfun = styling === 'cssfun';
68
+
69
+ // Router
70
+ const router = await p.confirm({
71
+ message : 'Add micro-router?',
72
+ initialValue : defaults.router || false
73
+ });
74
+
75
+ if (p.isCancel(router)) {
76
+ p.cancel('Operation cancelled.');
77
+ return null;
78
+ }
79
+
80
+ options.router = router;
81
+
82
+ // Icons
83
+ const addIcons = await p.confirm({
84
+ message : 'Add icon components?',
85
+ initialValue : !!defaults.icons
86
+ });
87
+
88
+ if (p.isCancel(addIcons)) {
89
+ p.cancel('Operation cancelled.');
90
+ return null;
91
+ }
92
+
93
+ if (addIcons) {
94
+ const initialIconSets = Array.isArray(defaults.icons)
95
+ ? defaults.icons
96
+ : (defaults.icons ? [defaults.icons] : [DEFAULT_PRESET]);
97
+
98
+ const iconsets = await p.multiselect({
99
+ message : 'Which icon sets?',
100
+ options : PRESET_OPTIONS,
101
+ initialValues : initialIconSets,
102
+ required : true
103
+ });
104
+
105
+ if (p.isCancel(iconsets)) {
106
+ p.cancel('Operation cancelled.');
107
+ return null;
108
+ }
109
+
110
+ options.icons = iconsets;
111
+ } else {
112
+ options.icons = null;
113
+ }
114
+
115
+ return options;
116
+ }
@@ -0,0 +1,21 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ /**
5
+ * Write content to a file, creating directories if needed.
6
+ * @param {string} filePath - Path to the file
7
+ * @param {string} content - Content to write
8
+ */
9
+ export async function writeFile(filePath, content) {
10
+ await fs.mkdir(path.dirname(filePath), { recursive : true });
11
+ await fs.writeFile(filePath, content, 'utf-8');
12
+ }
13
+
14
+ /**
15
+ * Read content from a file.
16
+ * @param {string} filePath - Path to the file
17
+ * @returns {Promise<string>} File content
18
+ */
19
+ export async function readFile(filePath) {
20
+ return fs.readFile(filePath, 'utf-8');
21
+ }