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,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { generateIcons } from '../src/generate.js';
|
|
4
|
+
import { DEFAULT_PRESET, PRESETS } from '../src/presets.js';
|
|
5
|
+
|
|
6
|
+
function parseArgs(argv) {
|
|
7
|
+
const args = { preset : null, source : null, license : null, output : null };
|
|
8
|
+
for (let i = 0; i < argv.length; i++) {
|
|
9
|
+
const arg = argv[i];
|
|
10
|
+
if (arg === '--preset') { args.preset = argv[++i]; }
|
|
11
|
+
else if (arg === '--source') { args.source = argv[++i]; }
|
|
12
|
+
else if (arg === '--license') { args.license = argv[++i]; }
|
|
13
|
+
else if (arg === '--output') { args.output = argv[++i]; }
|
|
14
|
+
}
|
|
15
|
+
return args;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function showHelp() {
|
|
19
|
+
const presetList = Object.keys(PRESETS).map(k => ` ${k}`).join('\n');
|
|
20
|
+
console.log(`
|
|
21
|
+
rasti-icons — Generate Rasti icon components from GitHub-hosted SVG sets
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
rasti-icons --preset <name> [--output <dir>]
|
|
25
|
+
rasti-icons --source <github-api-url> [--license <url>] --output <dir>
|
|
26
|
+
|
|
27
|
+
Options:
|
|
28
|
+
--preset <name> Built-in preset (see below)
|
|
29
|
+
--source <url> GitHub API URL for a directory listing of SVG files
|
|
30
|
+
--license <url> Raw URL for the license file (optional)
|
|
31
|
+
--output <dir> Output directory (default: ./icons/<preset> or ./icons)
|
|
32
|
+
|
|
33
|
+
Presets:
|
|
34
|
+
${presetList}
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
rasti-icons --preset ${DEFAULT_PRESET}
|
|
38
|
+
rasti-icons --preset heroicons-outline --output ./src/icons
|
|
39
|
+
rasti-icons --preset akar-icons --output ./src/icons
|
|
40
|
+
rasti-icons --preset feathericon --output ./src/icons
|
|
41
|
+
rasti-icons --preset pixelarticons --output ./src/icons
|
|
42
|
+
rasti-icons --source https://api.github.com/repos/owner/repo/contents/svg --output ./icons
|
|
43
|
+
`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function main() {
|
|
47
|
+
const argv = process.argv.slice(2);
|
|
48
|
+
|
|
49
|
+
if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) {
|
|
50
|
+
showHelp();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const args = parseArgs(argv);
|
|
55
|
+
|
|
56
|
+
let source = args.source;
|
|
57
|
+
let license = args.license;
|
|
58
|
+
let outputDir = args.output;
|
|
59
|
+
|
|
60
|
+
if (args.preset) {
|
|
61
|
+
const preset = PRESETS[args.preset];
|
|
62
|
+
if (!preset) {
|
|
63
|
+
console.error(`Unknown preset "${args.preset}". Valid presets: ${Object.keys(PRESETS).join(', ')}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
source = source || preset.source;
|
|
67
|
+
license = license || preset.license;
|
|
68
|
+
if (!outputDir) outputDir = `./icons/${args.preset}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!source) {
|
|
72
|
+
showHelp();
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!outputDir) outputDir = './icons';
|
|
77
|
+
|
|
78
|
+
const output = path.resolve(process.cwd(), outputDir);
|
|
79
|
+
console.log(`Fetching icons from ${source}...`);
|
|
80
|
+
const count = await generateIcons({ source, license, output });
|
|
81
|
+
console.log(`Done. Generated ${count} icon${count !== 1 ? 's' : ''} → ${output}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
main().catch(e => { console.error(e.message); process.exit(1); });
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name" : "rasti-icons",
|
|
3
|
+
"version" : "0.0.0",
|
|
4
|
+
"private" : true,
|
|
5
|
+
"description" : "Generic Rasti icon component generator from GitHub-hosted SVG sets",
|
|
6
|
+
"type" : "module",
|
|
7
|
+
"bin" : {
|
|
8
|
+
"rasti-icons" : "./bin/rasti-icons.js"
|
|
9
|
+
},
|
|
10
|
+
"license" : "MIT"
|
|
11
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Convert kebab-case SVG filename to PascalCase component name.
|
|
6
|
+
* @param {string} filename - e.g. 'arrow-right.svg'
|
|
7
|
+
* @returns {string} e.g. 'ArrowRight'
|
|
8
|
+
*/
|
|
9
|
+
function toPascalCase(filename) {
|
|
10
|
+
return filename
|
|
11
|
+
.replace('.svg', '')
|
|
12
|
+
.replace(/-(.)/g, (_, c) => c.toUpperCase())
|
|
13
|
+
.replace(/^(.)/, m => m.toUpperCase());
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extract a size value from the SVG opening tag.
|
|
18
|
+
* Tries direct attribute first, falls back to viewBox.
|
|
19
|
+
* @param {string} tag - The opening <svg ...> tag string
|
|
20
|
+
* @param {'width'|'height'} dim - Dimension to extract
|
|
21
|
+
* @returns {string} Numeric string, e.g. '24'
|
|
22
|
+
*/
|
|
23
|
+
function extractSize(tag, dim) {
|
|
24
|
+
// Match only real SVG width/height attributes (not stroke-width, etc.)
|
|
25
|
+
const direct = tag.match(new RegExp(`\\s${dim}="(\\d+(?:\\.\\d+)?)"`));
|
|
26
|
+
if (direct) return direct[1];
|
|
27
|
+
const vb = tag.match(/\bviewBox="[\d.]+ [\d.]+ ([\d.]+) ([\d.]+)"/);
|
|
28
|
+
if (vb) return dim === 'width' ? vb[1] : vb[2];
|
|
29
|
+
return '24';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Transform raw SVG content into a Rasti component template body.
|
|
34
|
+
* Replaces/adds class, width, height with dynamic props expressions.
|
|
35
|
+
* @param {string} svgContent - Raw SVG file content
|
|
36
|
+
* @returns {string} Indented SVG ready to embed in Component.create`...`
|
|
37
|
+
*/
|
|
38
|
+
function transformSvg(svgContent) {
|
|
39
|
+
const openEnd = svgContent.indexOf('>');
|
|
40
|
+
const openTag = svgContent.slice(0, openEnd + 1);
|
|
41
|
+
const rest = svgContent.slice(openEnd + 1);
|
|
42
|
+
|
|
43
|
+
const origW = extractSize(openTag, 'width');
|
|
44
|
+
const origH = extractSize(openTag, 'height');
|
|
45
|
+
|
|
46
|
+
// Build template expression strings (written as literal text to output files)
|
|
47
|
+
const classExpr = 'class="${({ props }) => props.className || \'\'}"';
|
|
48
|
+
const widthExpr = `width="\${({ props }) => props.width || '${origW}'}"`;
|
|
49
|
+
const heightExpr = `height="\${({ props }) => props.height || '${origH}'}"`;
|
|
50
|
+
|
|
51
|
+
const newTag = openTag
|
|
52
|
+
.replace(/\s+class="[^"]*"/g, '')
|
|
53
|
+
.replace(/\s+width="[^"]*"/g, '')
|
|
54
|
+
.replace(/\s+height="[^"]*"/g, '')
|
|
55
|
+
.replace('<svg', `<svg ${classExpr} ${widthExpr} ${heightExpr}`);
|
|
56
|
+
|
|
57
|
+
return (newTag + rest)
|
|
58
|
+
.replace(/\n$/, '')
|
|
59
|
+
.replace(/^ {2}/gm, ' ')
|
|
60
|
+
.replace(/^/gm, ' ');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Generate Rasti icon components from a GitHub-hosted SVG directory.
|
|
65
|
+
* @param {object} options
|
|
66
|
+
* @param {string} options.source - GitHub API URL for directory listing
|
|
67
|
+
* @param {string} [options.license] - Raw URL for license file (optional)
|
|
68
|
+
* @param {string} options.output - Absolute path to output directory
|
|
69
|
+
* @returns {Promise<number>} Number of icons generated
|
|
70
|
+
*/
|
|
71
|
+
export async function generateIcons({ source, license, output }) {
|
|
72
|
+
fs.mkdirSync(output, { recursive : true });
|
|
73
|
+
|
|
74
|
+
if (license) {
|
|
75
|
+
const licRes = await fetch(license);
|
|
76
|
+
if (!licRes.ok) throw new Error(`License fetch failed: ${licRes.status}`);
|
|
77
|
+
fs.writeFileSync(path.join(output, 'LICENSE.txt'), await licRes.text());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const listRes = await fetch(source);
|
|
81
|
+
if (!listRes.ok) throw new Error(`Source fetch failed: ${listRes.status}`);
|
|
82
|
+
const files = await listRes.json();
|
|
83
|
+
|
|
84
|
+
if (!Array.isArray(files)) {
|
|
85
|
+
throw new Error(`Unexpected response from source URL. Got: ${JSON.stringify(files).slice(0, 100)}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let count = 0;
|
|
89
|
+
for (const item of files) {
|
|
90
|
+
if (item.type !== 'file' || !item.name.endsWith('.svg')) continue;
|
|
91
|
+
const iconName = toPascalCase(item.name);
|
|
92
|
+
const svgRes = await fetch(item.download_url);
|
|
93
|
+
if (!svgRes.ok) throw new Error(`SVG fetch failed for ${item.name}: ${svgRes.status}`);
|
|
94
|
+
const svgContent = await svgRes.text();
|
|
95
|
+
const transformed = transformSvg(svgContent);
|
|
96
|
+
const component = [
|
|
97
|
+
`import { Component } from 'rasti';`,
|
|
98
|
+
``,
|
|
99
|
+
`/**`,
|
|
100
|
+
` * @typedef {Object} IconProps`,
|
|
101
|
+
` * @property {string} [className]`,
|
|
102
|
+
` * @property {string} [width]`,
|
|
103
|
+
` * @property {string} [height]`,
|
|
104
|
+
` */`,
|
|
105
|
+
``,
|
|
106
|
+
`/** @type {typeof import('rasti').Component<IconProps>} */`,
|
|
107
|
+
`const ${iconName} = Component.create\``,
|
|
108
|
+
transformed,
|
|
109
|
+
`\`;`,
|
|
110
|
+
``,
|
|
111
|
+
`export default ${iconName};`,
|
|
112
|
+
``
|
|
113
|
+
].join('\n');
|
|
114
|
+
fs.writeFileSync(path.join(output, `${iconName}.js`), component);
|
|
115
|
+
count++;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return count;
|
|
119
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export const DEFAULT_PRESET = 'heroicons-outline';
|
|
2
|
+
|
|
3
|
+
export const PRESETS = {
|
|
4
|
+
'heroicons-outline' : {
|
|
5
|
+
label : 'Heroicons Outline',
|
|
6
|
+
hint : '24px, by Tailwind Labs (MIT)',
|
|
7
|
+
sourceName : 'Heroicons by Tailwind Labs',
|
|
8
|
+
licenseName : 'MIT',
|
|
9
|
+
homepage : 'https://heroicons.com',
|
|
10
|
+
source : 'https://api.github.com/repos/tailwindlabs/heroicons/contents/optimized/24/outline',
|
|
11
|
+
license : 'https://raw.githubusercontent.com/tailwindlabs/heroicons/master/LICENSE'
|
|
12
|
+
},
|
|
13
|
+
'heroicons-solid' : {
|
|
14
|
+
label : 'Heroicons Solid',
|
|
15
|
+
hint : '24px, by Tailwind Labs (MIT)',
|
|
16
|
+
sourceName : 'Heroicons by Tailwind Labs',
|
|
17
|
+
licenseName : 'MIT',
|
|
18
|
+
homepage : 'https://heroicons.com',
|
|
19
|
+
source : 'https://api.github.com/repos/tailwindlabs/heroicons/contents/optimized/24/solid',
|
|
20
|
+
license : 'https://raw.githubusercontent.com/tailwindlabs/heroicons/master/LICENSE'
|
|
21
|
+
},
|
|
22
|
+
pixelarticons : {
|
|
23
|
+
label : 'Pixelarticons',
|
|
24
|
+
hint : 'by Halfmage (MIT)',
|
|
25
|
+
sourceName : 'Pixelarticons by Halfmage',
|
|
26
|
+
licenseName : 'MIT',
|
|
27
|
+
homepage : 'https://pixelarticons.com',
|
|
28
|
+
source : 'https://api.github.com/repos/halfmage/pixelarticons/contents/svg',
|
|
29
|
+
license : 'https://raw.githubusercontent.com/halfmage/pixelarticons/master/LICENSE'
|
|
30
|
+
},
|
|
31
|
+
'akar-icons' : {
|
|
32
|
+
label : 'Akar Icons',
|
|
33
|
+
hint : '24px, by Arturo Wibawa (MIT)',
|
|
34
|
+
sourceName : 'Akar Icons by Arturo Wibawa',
|
|
35
|
+
licenseName : 'MIT',
|
|
36
|
+
homepage : 'https://akaricons.com',
|
|
37
|
+
source : 'https://api.github.com/repos/artcoholic/akar-icons/contents/src/svg',
|
|
38
|
+
license : 'https://raw.githubusercontent.com/artcoholic/akar-icons/master/LICENSE'
|
|
39
|
+
},
|
|
40
|
+
feathericon : {
|
|
41
|
+
label : 'Feathericon',
|
|
42
|
+
hint : 'by Megumi Hano (MIT)',
|
|
43
|
+
sourceName : 'Feathericon by Megumi Hano',
|
|
44
|
+
licenseName : 'MIT',
|
|
45
|
+
homepage : 'https://feathericon.github.io/feathericon/',
|
|
46
|
+
source : 'https://api.github.com/repos/feathericon/feathericon/contents/src/svg',
|
|
47
|
+
license : 'https://raw.githubusercontent.com/feathericon/feathericon/master/LICENSE'
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const PRESET_IDS = Object.keys(PRESETS);
|
|
52
|
+
|
|
53
|
+
export const PRESET_OPTIONS = PRESET_IDS.map((value) => {
|
|
54
|
+
const { label, hint } = PRESETS[value];
|
|
55
|
+
|
|
56
|
+
return { value, label, hint };
|
|
57
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-rasti",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Create Rasti projects with Vite",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-rasti": "./bin/create-rasti.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"templates",
|
|
13
|
+
"extras",
|
|
14
|
+
"!extras/**/node_modules"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"prepare": "npm run test",
|
|
18
|
+
"lint": "eslint src bin extras",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"posttest": "npm run lint",
|
|
21
|
+
"version": "node scripts/release-changelog.js"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/8tentaculos/create-rasti.git"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/8tentaculos/create-rasti",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/8tentaculos/create-rasti/issues"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"rasti",
|
|
33
|
+
"vite",
|
|
34
|
+
"starter",
|
|
35
|
+
"scaffolding",
|
|
36
|
+
"cli",
|
|
37
|
+
"spa",
|
|
38
|
+
"ssr"
|
|
39
|
+
],
|
|
40
|
+
"author": "Alberto Masuelli <alberto.masuelli@gmail.com> (https://github.com/8tentaculos)",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@clack/prompts": "^0.9.1"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@eslint/js": "^9.34.0",
|
|
47
|
+
"eslint": "^9.34.0",
|
|
48
|
+
"globals": "^16.3.0",
|
|
49
|
+
"vitest": "^3.0.0"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=22"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -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 SPA base template.
|
|
8
|
+
* @param {object} ctx - Context object with plan and targetDir
|
|
9
|
+
*/
|
|
10
|
+
export async function applyBase(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('spa', 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,38 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import { copyTemplateFile } 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
|
+
* Apply cssfun to the project.
|
|
10
|
+
* @param {object} ctx - Context object with plan and targetDir
|
|
11
|
+
*/
|
|
12
|
+
export async function applyCssfun(ctx) {
|
|
13
|
+
const { plan, targetDir } = ctx;
|
|
14
|
+
const isSsr = plan.base === 'ssr' || plan.base === 'static';
|
|
15
|
+
const routeAbout = plan.base === 'static' ? '/about/' : '/about';
|
|
16
|
+
const context = {
|
|
17
|
+
...getBaseContext(plan),
|
|
18
|
+
ROUTER : plan.features.router ? 'true' : '',
|
|
19
|
+
ROUTE_ABOUT : routeAbout
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
await addDependencies(targetDir, {
|
|
23
|
+
dependencies : {
|
|
24
|
+
'cssfun' : VERSIONS.cssfun
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// App and components (Button, Home, Header) are resolved by copyStyledTemplate
|
|
29
|
+
// in the base appliers, which pick the *-cssfun.js variants when cssfun is enabled.
|
|
30
|
+
await copyTemplateFile('_features/cssfun/theme.js', path.join(targetDir, 'src', 'theme.js'), {});
|
|
31
|
+
|
|
32
|
+
// cssfun handles all styling — drop the base style.css.
|
|
33
|
+
await fs.rm(path.join(targetDir, 'src', 'style.css'), { force : true });
|
|
34
|
+
|
|
35
|
+
if (isSsr) {
|
|
36
|
+
await copyTemplateFile('_features/cssfun/index.html', path.join(targetDir, 'index.html'), context);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { VERSIONS } from '../versions.js';
|
|
2
|
+
|
|
3
|
+
const CDN_BASE = 'https://cdn.jsdelivr.net/gh/8tentaculos/rasti@v' + VERSIONS.rasti.replace(/^[\^~]/, '') + '/docs';
|
|
4
|
+
|
|
5
|
+
/** Logo and favicon URLs served from the Rasti CDN. */
|
|
6
|
+
const LOGO_SRC = CDN_BASE + '/logo-dark.svg';
|
|
7
|
+
const LOGO_LIGHT_SRC = CDN_BASE + '/logo.svg';
|
|
8
|
+
const FAVICON_SRC = CDN_BASE + '/favicon.svg';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Shared placeholder context for template copying.
|
|
12
|
+
* @param {object} plan - Project plan
|
|
13
|
+
* @returns {object} Placeholder map for copyTemplateFile/copyTemplateDir
|
|
14
|
+
*/
|
|
15
|
+
export function getBaseContext(plan) {
|
|
16
|
+
return {
|
|
17
|
+
NAME : plan.name,
|
|
18
|
+
DESCRIPTION : getDescription(plan),
|
|
19
|
+
LOGO_SRC,
|
|
20
|
+
LOGO_LIGHT_SRC,
|
|
21
|
+
FAVICON_SRC,
|
|
22
|
+
RASTI_VERSION : VERSIONS.rasti,
|
|
23
|
+
VITE_VERSION : VERSIONS.vite,
|
|
24
|
+
EXPRESS_VERSION : VERSIONS.express,
|
|
25
|
+
COMPRESSION_VERSION : VERSIONS.compression,
|
|
26
|
+
SIRV_VERSION : VERSIONS.sirv,
|
|
27
|
+
CROSS_ENV_VERSION : VERSIONS.crossEnv
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build the {{DESCRIPTION}} HTML for a given plan.
|
|
33
|
+
* Centralizes the description logic used by base, ssr, and router appliers.
|
|
34
|
+
* @param {object} plan - Project plan
|
|
35
|
+
* @returns {string} HTML description string
|
|
36
|
+
*/
|
|
37
|
+
export function getDescription(plan) {
|
|
38
|
+
const isSsr = plan.base === 'ssr' || plan.base === 'static';
|
|
39
|
+
const parts = ['<a href="https://rasti.js.org" target="_blank">Rasti</a>', '<a href="https://vite.dev" target="_blank">Vite</a>'];
|
|
40
|
+
|
|
41
|
+
if (isSsr) parts.push('<a href="https://expressjs.com" target="_blank">Express</a>');
|
|
42
|
+
|
|
43
|
+
let styling = '';
|
|
44
|
+
if (plan.features.tailwind) {
|
|
45
|
+
styling = ', styled with <a href="https://tailwindcss.com" target="_blank">Tailwind CSS</a>';
|
|
46
|
+
} else if (plan.features.cssfun) {
|
|
47
|
+
styling = ', styled with <a href="https://cssfun.js.org" target="_blank">CSSFUN</a> (CSS-in-JS with light and dark themes)';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (plan.base === 'static') {
|
|
51
|
+
return `Pre-rendered static site with ${parts.join(' + ')}${styling}. Development uses SSR; run <code>npm run build:static</code> to export configured URLs to static HTML under <code>dist/static</code>.`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const prefix = isSsr ? 'Server-side rendered app with' : 'Single page app powered by';
|
|
55
|
+
return `${prefix} ${parts.join(' + ')}${styling}.`;
|
|
56
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import { PRESETS } from '../../extras/rasti-icons/src/presets.js';
|
|
4
|
+
|
|
5
|
+
const CREATE_RASTI_REPO = 'https://github.com/8tentaculos/create-rasti/tree/master';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build the HTML for the features info block from plan.
|
|
9
|
+
* @param {object} plan - Plan with features and base
|
|
10
|
+
* @returns {string} HTML fragment for the info block
|
|
11
|
+
*/
|
|
12
|
+
function buildFeaturesHtml(plan) {
|
|
13
|
+
const items = [];
|
|
14
|
+
|
|
15
|
+
if (plan.features.router) {
|
|
16
|
+
items.push(`<p>Router: <a href="${CREATE_RASTI_REPO}/extras/micro-router" target="_blank" rel="noopener">micro-router</a></p>`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (plan.features.icons) {
|
|
20
|
+
const iconSets = plan.features.icons
|
|
21
|
+
.map((presetId) => {
|
|
22
|
+
const preset = PRESETS[presetId];
|
|
23
|
+
return `<a href="${preset.homepage}" target="_blank" rel="noopener">${preset.label}</a>`;
|
|
24
|
+
})
|
|
25
|
+
.join(', ');
|
|
26
|
+
|
|
27
|
+
items.push(`<p>Icons: <a href="${CREATE_RASTI_REPO}/extras/rasti-icons" target="_blank" rel="noopener">rasti-icons</a> (${iconSets})</p>`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (items.length === 0) {
|
|
31
|
+
items.push('<p>No optional extras included.</p>');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return items.join('\n');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Replace {{FEATURES_INCLUDE}} in a file with the features list HTML.
|
|
39
|
+
* @param {string} filePath - Path to the file
|
|
40
|
+
* @param {string} html - Features list HTML
|
|
41
|
+
*/
|
|
42
|
+
async function replaceInFile(filePath, html) {
|
|
43
|
+
try {
|
|
44
|
+
let content = await fs.readFile(filePath, 'utf-8');
|
|
45
|
+
if (!content.includes('{{FEATURES_INCLUDE}}')) return;
|
|
46
|
+
content = content.replaceAll('{{FEATURES_INCLUDE}}', html);
|
|
47
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
48
|
+
} catch {
|
|
49
|
+
// file may not exist
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Replace {{FEATURES_INCLUDE}} in App.js and in all pages (when router).
|
|
55
|
+
* @param {object} ctx - Context with plan, targetDir
|
|
56
|
+
*/
|
|
57
|
+
export async function applyFeaturesInclude(ctx) {
|
|
58
|
+
const { plan, targetDir } = ctx;
|
|
59
|
+
const html = buildFeaturesHtml(plan);
|
|
60
|
+
|
|
61
|
+
const homePath = path.join(targetDir, 'src', 'components', 'Home.js');
|
|
62
|
+
await replaceInFile(homePath, html);
|
|
63
|
+
|
|
64
|
+
const pagesDir = path.join(targetDir, 'src', 'pages');
|
|
65
|
+
try {
|
|
66
|
+
const entries = await fs.readdir(pagesDir, { withFileTypes : true });
|
|
67
|
+
for (const e of entries) {
|
|
68
|
+
if (e.isFile() && (e.name.endsWith('.js') || e.name.endsWith('.ts'))) {
|
|
69
|
+
await replaceInFile(path.join(pagesDir, e.name), html);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
// no pages dir (no router)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { generateIcons } from '../../extras/rasti-icons/src/generate.js';
|
|
3
|
+
import { PRESETS } from '../../extras/rasti-icons/src/presets.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Add icons: fetch SVG components from the selected icon set(s) and write under src/icons/.
|
|
7
|
+
* One set → flat `src/icons/`; multiple sets → `src/icons/<preset>/` each.
|
|
8
|
+
* @param {object} ctx - Context with plan, targetDir
|
|
9
|
+
*/
|
|
10
|
+
export async function applyIcons(ctx) {
|
|
11
|
+
const { plan, targetDir } = ctx;
|
|
12
|
+
const sets = plan.features.icons;
|
|
13
|
+
const baseDir = path.join(targetDir, 'src', 'icons');
|
|
14
|
+
const useSubdirs = sets.length > 1;
|
|
15
|
+
|
|
16
|
+
for (const key of sets) {
|
|
17
|
+
const preset = PRESETS[key];
|
|
18
|
+
const output = useSubdirs ? path.join(baseDir, key) : baseDir;
|
|
19
|
+
await generateIcons({ source : preset.source, license : preset.license, output });
|
|
20
|
+
}
|
|
21
|
+
}
|