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.
- package/LICENSE +22 -0
- package/README.md +96 -0
- package/bin/create-rasti.js +4 -0
- package/extras/cn/README.md +57 -0
- package/extras/cn/package.json +9 -0
- package/extras/cn/src/index.js +37 -0
- package/extras/micro-router/README.md +78 -0
- package/extras/micro-router/package-lock.json +26 -0
- package/extras/micro-router/package.json +12 -0
- package/extras/micro-router/src/index.js +192 -0
- package/extras/rasti-icons/README.md +65 -0
- package/extras/rasti-icons/bin/rasti-icons.js +84 -0
- package/extras/rasti-icons/package.json +11 -0
- package/extras/rasti-icons/src/generate.js +119 -0
- package/extras/rasti-icons/src/presets.js +57 -0
- package/package.json +54 -0
- package/src/apply/base.js +29 -0
- package/src/apply/cssfun.js +38 -0
- package/src/apply/description.js +56 -0
- package/src/apply/featuresInclude.js +75 -0
- package/src/apply/icons.js +21 -0
- package/src/apply/index.js +134 -0
- package/src/apply/router.js +50 -0
- package/src/apply/ssr.js +29 -0
- package/src/apply/static.js +46 -0
- package/src/apply/tailwind.js +33 -0
- package/src/args.js +55 -0
- package/src/cli.js +91 -0
- package/src/plan.js +33 -0
- package/src/prompts.js +116 -0
- package/src/utils/copy.js +21 -0
- package/src/utils/exec.js +83 -0
- package/src/utils/logger.js +79 -0
- package/src/utils/pkg.js +87 -0
- package/src/utils/template.js +205 -0
- package/src/validate.js +48 -0
- package/src/versions.js +17 -0
- package/templates/AGENTS.md +48 -0
- package/templates/_base/App-cssfun.js +88 -0
- package/templates/_base/App-tailwind.js +58 -0
- package/templates/_base/App.js +58 -0
- package/templates/_base/components/Button-cssfun.js +51 -0
- package/templates/_base/components/Button-tailwind.js +52 -0
- package/templates/_base/components/Button.js +22 -0
- package/templates/_base/components/Header-cssfun.js +69 -0
- package/templates/_base/components/Header-tailwind.js +17 -0
- package/templates/_base/components/Header.js +17 -0
- package/templates/_base/components/Home-cssfun.js +98 -0
- package/templates/_base/components/Home-tailwind.js +35 -0
- package/templates/_base/components/Home.js +35 -0
- package/templates/_base/style.css +170 -0
- package/templates/_extras/router/components/About-cssfun.js +43 -0
- package/templates/_extras/router/components/About-tailwind.js +14 -0
- package/templates/_extras/router/components/About.js +16 -0
- package/templates/_extras/router/router-setup.js +60 -0
- package/templates/_features/cssfun/index.html +14 -0
- package/templates/_features/cssfun/theme.js +60 -0
- package/templates/_features/tailwind/style.css +26 -0
- package/templates/_features/tailwind/vite.config.js +8 -0
- package/templates/spa/index.html +14 -0
- package/templates/spa/package.json +17 -0
- package/templates/spa/public/.gitkeep +0 -0
- package/templates/spa/src/main.js +15 -0
- package/templates/spa/vite.config.js +6 -0
- package/templates/ssr/app.js +71 -0
- package/templates/ssr/index.html +16 -0
- package/templates/ssr/package.json +23 -0
- package/templates/ssr/public/.gitkeep +0 -0
- package/templates/ssr/server.js +7 -0
- package/templates/ssr/src/entry-client.js +15 -0
- package/templates/ssr/src/entry-server.js +49 -0
- package/templates/ssr/vite.config.js +6 -0
- package/templates/static/scripts/build-static.js +161 -0
- package/templates/static/scripts/serve-static.js +19 -0
- 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
|
+
}
|
package/src/apply/ssr.js
ADDED
|
@@ -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
|
+
}
|