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,83 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detect the package manager being used.
|
|
5
|
+
* Checks npm_config_user_agent or falls back to checking available commands.
|
|
6
|
+
* @returns {string} Package manager name (npm, pnpm, bun)
|
|
7
|
+
*/
|
|
8
|
+
export function detectPackageManager() {
|
|
9
|
+
const userAgent = process.env.npm_config_user_agent || '';
|
|
10
|
+
|
|
11
|
+
if (userAgent.includes('pnpm')) {
|
|
12
|
+
return 'pnpm';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (userAgent.includes('bun')) {
|
|
16
|
+
return 'bun';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (userAgent.includes('yarn')) {
|
|
20
|
+
return 'yarn';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Fallback: check if pnpm or bun is available
|
|
24
|
+
if (isCommandAvailable('pnpm')) {
|
|
25
|
+
return 'pnpm';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (isCommandAvailable('bun')) {
|
|
29
|
+
return 'bun';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return 'npm';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if a command is available in the system.
|
|
37
|
+
* @param {string} command - Command name
|
|
38
|
+
* @returns {boolean} True if command is available
|
|
39
|
+
*/
|
|
40
|
+
function isCommandAvailable(command) {
|
|
41
|
+
try {
|
|
42
|
+
execSync(`${command} --version`, { stdio : 'ignore' });
|
|
43
|
+
return true;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get the install command for the detected package manager.
|
|
51
|
+
* @param {string} pm - Package manager name
|
|
52
|
+
* @returns {string} Install command
|
|
53
|
+
*/
|
|
54
|
+
export function getInstallCommand(pm) {
|
|
55
|
+
switch (pm) {
|
|
56
|
+
case 'pnpm':
|
|
57
|
+
return 'pnpm install';
|
|
58
|
+
case 'bun':
|
|
59
|
+
return 'bun install';
|
|
60
|
+
case 'yarn':
|
|
61
|
+
return 'yarn';
|
|
62
|
+
default:
|
|
63
|
+
return 'npm install';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get the dev command for the detected package manager.
|
|
69
|
+
* @param {string} pm - Package manager name
|
|
70
|
+
* @returns {string} Dev command
|
|
71
|
+
*/
|
|
72
|
+
export function getDevCommand(pm) {
|
|
73
|
+
switch (pm) {
|
|
74
|
+
case 'pnpm':
|
|
75
|
+
return 'pnpm dev';
|
|
76
|
+
case 'bun':
|
|
77
|
+
return 'bun dev';
|
|
78
|
+
case 'yarn':
|
|
79
|
+
return 'yarn dev';
|
|
80
|
+
default:
|
|
81
|
+
return 'npm run dev';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { getInstallCommand, getDevCommand } from './exec.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Logging utilities using @clack/prompts for consistent output.
|
|
6
|
+
*/
|
|
7
|
+
export const log = {
|
|
8
|
+
/**
|
|
9
|
+
* Log an info message.
|
|
10
|
+
* @param {string} message - Message to log
|
|
11
|
+
*/
|
|
12
|
+
info : (message) => {
|
|
13
|
+
p.log.info(message);
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Log a success message.
|
|
18
|
+
* @param {string} message - Message to log
|
|
19
|
+
*/
|
|
20
|
+
success : (message) => {
|
|
21
|
+
p.log.success(message);
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Log a warning message.
|
|
26
|
+
* @param {string} message - Message to log
|
|
27
|
+
*/
|
|
28
|
+
warn : (message) => {
|
|
29
|
+
p.log.warn(message);
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Log an error message.
|
|
34
|
+
* @param {string} message - Message to log
|
|
35
|
+
*/
|
|
36
|
+
error : (message) => {
|
|
37
|
+
p.log.error(message);
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Log a step message.
|
|
42
|
+
* @param {string} message - Message to log
|
|
43
|
+
*/
|
|
44
|
+
step : (message) => {
|
|
45
|
+
p.log.step(message);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create a spinner for long-running operations.
|
|
51
|
+
* @returns {object} Spinner object with start and stop methods
|
|
52
|
+
*/
|
|
53
|
+
export function createSpinner() {
|
|
54
|
+
return p.spinner();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Show intro message.
|
|
59
|
+
*/
|
|
60
|
+
export function intro() {
|
|
61
|
+
p.intro('create-rasti');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Show outro message with next steps.
|
|
66
|
+
* @param {string} projectName - Name of the created project
|
|
67
|
+
* @param {string} [pm='npm'] - Package manager to use
|
|
68
|
+
*/
|
|
69
|
+
export function outro(projectName, pm = 'npm') {
|
|
70
|
+
const installCmd = getInstallCommand(pm);
|
|
71
|
+
const devCmd = getDevCommand(pm);
|
|
72
|
+
|
|
73
|
+
p.note(
|
|
74
|
+
`cd ${projectName}\n${installCmd}\n${devCmd}`,
|
|
75
|
+
'Next steps'
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
p.outro('Project ready. Have fun.');
|
|
79
|
+
}
|
package/src/utils/pkg.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { readFile, writeFile } from './copy.js';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Read package.json from a directory.
|
|
6
|
+
* @param {string} dir - Directory path
|
|
7
|
+
* @returns {Promise<object>} Package.json content
|
|
8
|
+
*/
|
|
9
|
+
export async function readPackageJson(dir) {
|
|
10
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
11
|
+
const content = await readFile(pkgPath);
|
|
12
|
+
return JSON.parse(content);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Write package.json to a directory.
|
|
17
|
+
* @param {string} dir - Directory path
|
|
18
|
+
* @param {object} pkg - Package.json content
|
|
19
|
+
*/
|
|
20
|
+
export async function writePackageJson(dir, pkg) {
|
|
21
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
22
|
+
// Use 4 spaces indentation to match Rasti style
|
|
23
|
+
const content = JSON.stringify(pkg, null, 4);
|
|
24
|
+
await writeFile(pkgPath, content + '\n');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Add dependencies to package.json.
|
|
29
|
+
* @param {string} dir - Directory path
|
|
30
|
+
* @param {object} deps - Dependencies to add { dependencies?: object, devDependencies?: object }
|
|
31
|
+
*/
|
|
32
|
+
export async function addDependencies(dir, deps) {
|
|
33
|
+
const pkg = await readPackageJson(dir);
|
|
34
|
+
|
|
35
|
+
if (deps.dependencies) {
|
|
36
|
+
pkg.dependencies = pkg.dependencies || {};
|
|
37
|
+
Object.assign(pkg.dependencies, deps.dependencies);
|
|
38
|
+
// Sort dependencies
|
|
39
|
+
pkg.dependencies = sortObject(pkg.dependencies);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (deps.devDependencies) {
|
|
43
|
+
pkg.devDependencies = pkg.devDependencies || {};
|
|
44
|
+
Object.assign(pkg.devDependencies, deps.devDependencies);
|
|
45
|
+
// Sort devDependencies
|
|
46
|
+
pkg.devDependencies = sortObject(pkg.devDependencies);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await writePackageJson(dir, pkg);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Update package.json name field.
|
|
54
|
+
* @param {string} dir - Directory path
|
|
55
|
+
* @param {string} name - New package name
|
|
56
|
+
*/
|
|
57
|
+
export async function updatePackageName(dir, name) {
|
|
58
|
+
const pkg = await readPackageJson(dir);
|
|
59
|
+
pkg.name = name;
|
|
60
|
+
await writePackageJson(dir, pkg);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Merge scripts into package.json (add or override).
|
|
65
|
+
* @param {string} dir - Directory path
|
|
66
|
+
* @param {object} scripts - Scripts to merge { scriptName: "command", ... }
|
|
67
|
+
*/
|
|
68
|
+
export async function mergeScripts(dir, scripts) {
|
|
69
|
+
const pkg = await readPackageJson(dir);
|
|
70
|
+
pkg.scripts = pkg.scripts || {};
|
|
71
|
+
Object.assign(pkg.scripts, scripts);
|
|
72
|
+
await writePackageJson(dir, pkg);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Sort object keys alphabetically.
|
|
77
|
+
* @param {object} obj - Object to sort
|
|
78
|
+
* @returns {object} Sorted object
|
|
79
|
+
*/
|
|
80
|
+
function sortObject(obj) {
|
|
81
|
+
return Object.keys(obj)
|
|
82
|
+
.sort()
|
|
83
|
+
.reduce((result, key) => {
|
|
84
|
+
result[key] = obj[key];
|
|
85
|
+
return result;
|
|
86
|
+
}, {});
|
|
87
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const TEMPLATES_DIR = path.resolve(__dirname, '../../templates');
|
|
7
|
+
const EXTRAS_DIR = path.resolve(__dirname, '../../extras');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Copy a file from the `extras/` directory verbatim (no placeholder processing).
|
|
11
|
+
* Used for standalone mini-packages copied into generated projects (e.g. cn, micro-router).
|
|
12
|
+
* @param {string} extraPath - Relative path inside extras/ (e.g. 'cn/src/index.js')
|
|
13
|
+
* @param {string} destPath - Absolute destination path
|
|
14
|
+
*/
|
|
15
|
+
export async function copyExtraFile(extraPath, destPath) {
|
|
16
|
+
const src = path.join(EXTRAS_DIR, extraPath);
|
|
17
|
+
const content = await fs.readFile(src, 'utf-8');
|
|
18
|
+
await fs.mkdir(path.dirname(destPath), { recursive : true });
|
|
19
|
+
await fs.writeFile(destPath, content, 'utf-8');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Process conditional blocks: {{#if KEY}}...{{#elif KEY}}...{{#else}}...{{#endif}}.
|
|
24
|
+
* Supports nesting. Evaluates KEY as truthy/falsy against context.
|
|
25
|
+
* @param {string} content - Template content
|
|
26
|
+
* @param {object} context - Values to evaluate conditions
|
|
27
|
+
* @returns {string} Processed content
|
|
28
|
+
*/
|
|
29
|
+
function processConditionals(content, context) {
|
|
30
|
+
const RE = /(?:^[ \t]*)?(?:\{\{#if\s+(\w+)\}\}|\{\{#elif\s+(\w+)\}\}|\{\{#else\}\}|\{\{#endif\}\})(?:[ \t]*$\n?)?/gm;
|
|
31
|
+
const stack = [];
|
|
32
|
+
let result = '';
|
|
33
|
+
let lastIndex = 0;
|
|
34
|
+
let match;
|
|
35
|
+
|
|
36
|
+
while ((match = RE.exec(content)) !== null) {
|
|
37
|
+
const [token, ifKey, elifKey] = match;
|
|
38
|
+
const textBefore = content.slice(lastIndex, match.index);
|
|
39
|
+
const shouldEmit = stack.length === 0 || stack[stack.length - 1].emit;
|
|
40
|
+
|
|
41
|
+
if (shouldEmit) result += textBefore;
|
|
42
|
+
lastIndex = RE.lastIndex;
|
|
43
|
+
|
|
44
|
+
if (ifKey) {
|
|
45
|
+
const parentEmit = stack.length === 0 || stack[stack.length - 1].emit;
|
|
46
|
+
const condTrue = parentEmit && !!context[ifKey];
|
|
47
|
+
stack.push({ resolved : condTrue, emit : condTrue });
|
|
48
|
+
} else if (elifKey) {
|
|
49
|
+
const frame = stack[stack.length - 1];
|
|
50
|
+
const parentEmit = stack.length <= 1 || stack[stack.length - 2].emit;
|
|
51
|
+
if (parentEmit && !frame.resolved && !!context[elifKey]) {
|
|
52
|
+
frame.emit = true;
|
|
53
|
+
frame.resolved = true;
|
|
54
|
+
} else {
|
|
55
|
+
frame.emit = false;
|
|
56
|
+
}
|
|
57
|
+
} else if (token.includes('#else')) {
|
|
58
|
+
const frame = stack[stack.length - 1];
|
|
59
|
+
const parentEmit = stack.length <= 1 || stack[stack.length - 2].emit;
|
|
60
|
+
frame.emit = parentEmit && !frame.resolved;
|
|
61
|
+
if (frame.emit) frame.resolved = true;
|
|
62
|
+
} else {
|
|
63
|
+
stack.pop();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const tailEmit = stack.length === 0 || stack[stack.length - 1].emit;
|
|
68
|
+
if (tailEmit) result += content.slice(lastIndex);
|
|
69
|
+
|
|
70
|
+
// Collapse runs of 3+ newlines (left by conditional blocks) into 2
|
|
71
|
+
return result.replace(/\n{3,}/g, '\n\n');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Copy a template file processing placeholders.
|
|
76
|
+
* @param {string} templatePath - Relative path in templates/
|
|
77
|
+
* @param {string} destPath - Absolute destination path
|
|
78
|
+
* @param {object} context - Values to replace placeholders
|
|
79
|
+
*/
|
|
80
|
+
export async function copyTemplateFile(templatePath, destPath, context = {}) {
|
|
81
|
+
const src = path.join(TEMPLATES_DIR, templatePath);
|
|
82
|
+
let content = await fs.readFile(src, 'utf-8');
|
|
83
|
+
|
|
84
|
+
// Process conditional blocks first
|
|
85
|
+
content = processConditionals(content, context);
|
|
86
|
+
|
|
87
|
+
// Replace placeholders {{KEY}} with context values
|
|
88
|
+
for (const [key, value] of Object.entries(context)) {
|
|
89
|
+
content = content.replaceAll(`{{${key}}}`, value);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Ensure directory exists
|
|
93
|
+
await fs.mkdir(path.dirname(destPath), { recursive : true });
|
|
94
|
+
await fs.writeFile(destPath, content, 'utf-8');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Copy a template directory to the target, processing placeholders in all files.
|
|
99
|
+
* @param {string} templateName - Name of the template directory (spa, ssr)
|
|
100
|
+
* @param {string} targetDir - Target directory path
|
|
101
|
+
* @param {object} context - Values to replace placeholders
|
|
102
|
+
*/
|
|
103
|
+
export async function copyTemplateDir(templateName, targetDir, context = {}) {
|
|
104
|
+
const templateDir = path.join(TEMPLATES_DIR, templateName);
|
|
105
|
+
await copyDirRecursive(templateDir, targetDir, context);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Recursively copy a directory, processing placeholders in text files.
|
|
110
|
+
* @param {string} src - Source directory
|
|
111
|
+
* @param {string} dest - Destination directory
|
|
112
|
+
* @param {object} context - Values to replace placeholders
|
|
113
|
+
*/
|
|
114
|
+
async function copyDirRecursive(src, dest, context) {
|
|
115
|
+
await fs.mkdir(dest, { recursive : true });
|
|
116
|
+
|
|
117
|
+
const entries = await fs.readdir(src, { withFileTypes : true });
|
|
118
|
+
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
const srcPath = path.join(src, entry.name);
|
|
121
|
+
const destPath = path.join(dest, entry.name);
|
|
122
|
+
|
|
123
|
+
if (entry.isDirectory()) {
|
|
124
|
+
await copyDirRecursive(srcPath, destPath, context);
|
|
125
|
+
} else {
|
|
126
|
+
if (isTextFile(entry.name)) {
|
|
127
|
+
let content = await fs.readFile(srcPath, 'utf-8');
|
|
128
|
+
content = processConditionals(content, context);
|
|
129
|
+
|
|
130
|
+
for (const [key, value] of Object.entries(context)) {
|
|
131
|
+
content = content.replaceAll(`{{${key}}}`, value);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await fs.writeFile(destPath, content, 'utf-8');
|
|
135
|
+
} else {
|
|
136
|
+
await fs.copyFile(srcPath, destPath);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check if a file is a text file based on extension.
|
|
144
|
+
* @param {string} filename - File name
|
|
145
|
+
* @returns {boolean} True if text file
|
|
146
|
+
*/
|
|
147
|
+
function isTextFile(filename) {
|
|
148
|
+
const textExtensions = [
|
|
149
|
+
'.js', '.ts', '.jsx', '.tsx',
|
|
150
|
+
'.json', '.html', '.css', '.scss',
|
|
151
|
+
'.md', '.txt', '.svg', '.xml',
|
|
152
|
+
'.gitkeep', '.gitignore', '.env'
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
const ext = path.extname(filename).toLowerCase();
|
|
156
|
+
return textExtensions.includes(ext) || filename.startsWith('.');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Copy a template file, auto-resolving styling variant by convention.
|
|
161
|
+
* Given `dir/File.js` and plan with tailwind, looks for `dir/File-tailwind.js` first.
|
|
162
|
+
* Falls back to `dir/File-cssfun.js` if cssfun, then `dir/File.js`.
|
|
163
|
+
* @param {string} templatePath - Base template path (e.g. '_extras/router/App.js')
|
|
164
|
+
* @param {string} destPath - Absolute destination path
|
|
165
|
+
* @param {object} context - Values to replace placeholders
|
|
166
|
+
* @param {object} plan - Project plan (checks plan.features.tailwind / cssfun)
|
|
167
|
+
*/
|
|
168
|
+
export async function copyStyledTemplate(templatePath, destPath, context, plan) {
|
|
169
|
+
const ext = path.extname(templatePath);
|
|
170
|
+
const base = templatePath.slice(0, -ext.length);
|
|
171
|
+
|
|
172
|
+
let resolvedPath = templatePath;
|
|
173
|
+
if (plan.features.tailwind) {
|
|
174
|
+
const candidate = `${base}-tailwind${ext}`;
|
|
175
|
+
const full = path.join(TEMPLATES_DIR, candidate);
|
|
176
|
+
try { await fs.access(full); resolvedPath = candidate; } catch { /* fallback to base */ }
|
|
177
|
+
} else if (plan.features.cssfun) {
|
|
178
|
+
const candidate = `${base}-cssfun${ext}`;
|
|
179
|
+
const full = path.join(TEMPLATES_DIR, candidate);
|
|
180
|
+
try { await fs.access(full); resolvedPath = candidate; } catch { /* fallback to base */ }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await copyTemplateFile(resolvedPath, destPath, context);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Copy a directory from source path to destination (no placeholder processing).
|
|
188
|
+
* Used for copying generated output (e.g. extras/rasti-heroicons/src/icons) into the project.
|
|
189
|
+
* @param {string} sourceDir - Absolute path to source directory
|
|
190
|
+
* @param {string} destDir - Absolute path to destination directory
|
|
191
|
+
* @returns {Promise<void>}
|
|
192
|
+
*/
|
|
193
|
+
export async function copyDirFromTo(sourceDir, destDir) {
|
|
194
|
+
await fs.mkdir(destDir, { recursive : true });
|
|
195
|
+
const entries = await fs.readdir(sourceDir, { withFileTypes : true });
|
|
196
|
+
for (const entry of entries) {
|
|
197
|
+
const srcPath = path.join(sourceDir, entry.name);
|
|
198
|
+
const destPath = path.join(destDir, entry.name);
|
|
199
|
+
if (entry.isDirectory()) {
|
|
200
|
+
await copyDirFromTo(srcPath, destPath);
|
|
201
|
+
} else {
|
|
202
|
+
await fs.copyFile(srcPath, destPath);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
package/src/validate.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { PRESET_IDS } from '../extras/rasti-icons/src/presets.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validate a project plan.
|
|
5
|
+
* @param {object} plan - Project plan object
|
|
6
|
+
* @returns {object} Validation result { valid: boolean, error?: string }
|
|
7
|
+
*/
|
|
8
|
+
export function validatePlan(plan) {
|
|
9
|
+
// Check required fields
|
|
10
|
+
if (!plan.name) {
|
|
11
|
+
return { valid : false, error : 'Project name is required' };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Validate project name format
|
|
15
|
+
if (!/^[a-z0-9-_]+$/i.test(plan.name)) {
|
|
16
|
+
return {
|
|
17
|
+
valid : false,
|
|
18
|
+
error : 'Project name can only contain letters, numbers, dashes and underscores'
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Check base template
|
|
23
|
+
if (!['spa', 'ssr', 'static'].includes(plan.base)) {
|
|
24
|
+
return { valid : false, error : 'Invalid base template. Must be "spa", "ssr" or "static"' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Check icon sets
|
|
28
|
+
if (plan.features.icons) {
|
|
29
|
+
for (const id of plan.features.icons) {
|
|
30
|
+
if (!PRESET_IDS.includes(id)) {
|
|
31
|
+
return {
|
|
32
|
+
valid : false,
|
|
33
|
+
error : `Invalid icon set "${id}". Valid: ${PRESET_IDS.join(', ')}`
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check mutually exclusive features
|
|
40
|
+
if (plan.features.tailwind && plan.features.cssfun) {
|
|
41
|
+
return {
|
|
42
|
+
valid : false,
|
|
43
|
+
error : 'Cannot use both Tailwind and CSSFUN. They are mutually exclusive.'
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { valid : true };
|
|
48
|
+
}
|
package/src/versions.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for every version pinned in generated projects.
|
|
3
|
+
* When bumping any of these, also follow docs/VERSIONS.md (e.g. path-to-regexp
|
|
4
|
+
* is mirrored in extras/micro-router/package.json, kept in sync manually).
|
|
5
|
+
*/
|
|
6
|
+
export const VERSIONS = {
|
|
7
|
+
rasti : '^4.0.1',
|
|
8
|
+
vite : '^7.0.0',
|
|
9
|
+
express : '^5.0.1',
|
|
10
|
+
compression : '^1.8.1',
|
|
11
|
+
sirv : '^3.0.0',
|
|
12
|
+
crossEnv : '^7.0.3',
|
|
13
|
+
tailwindcss : '^4.0.0',
|
|
14
|
+
tailwindVite : '^4.0.0',
|
|
15
|
+
cssfun : '^0.0.14',
|
|
16
|
+
pathToRegexp : '^8.0.0'
|
|
17
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# {{NAME}}
|
|
2
|
+
|
|
3
|
+
{{#if STATIC}}
|
|
4
|
+
Pre-rendered static site with [Rasti](https://rasti.js.org) {{RASTI_VERSION}} + [Vite](https://vite.dev) + [Express](https://expressjs.com){{#if TAILWIND}}, styled with [Tailwind CSS](https://tailwindcss.com){{#endif}}{{#if CSSFUN}}, styled with [CSSFUN](https://cssfun.js.org){{#endif}}.
|
|
5
|
+
{{#elif SSR}}
|
|
6
|
+
Server-side rendered app with [Rasti](https://rasti.js.org) {{RASTI_VERSION}} + [Vite](https://vite.dev) + [Express](https://expressjs.com){{#if TAILWIND}}, styled with [Tailwind CSS](https://tailwindcss.com){{#endif}}{{#if CSSFUN}}, styled with [CSSFUN](https://cssfun.js.org){{#endif}}.
|
|
7
|
+
{{#else}}
|
|
8
|
+
Single page app powered by [Rasti](https://rasti.js.org) {{RASTI_VERSION}} + [Vite](https://vite.dev){{#if TAILWIND}}, styled with [Tailwind CSS](https://tailwindcss.com){{#endif}}{{#if CSSFUN}}, styled with [CSSFUN](https://cssfun.js.org){{#endif}}.
|
|
9
|
+
{{#endif}}
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm run dev # development server (HMR)
|
|
15
|
+
npm run build # production build
|
|
16
|
+
npm run preview # preview production build
|
|
17
|
+
{{#if SSR}}
|
|
18
|
+
npm start # start Express server (requires npm run build first)
|
|
19
|
+
{{#endif}}
|
|
20
|
+
{{#if STATIC}}
|
|
21
|
+
npm run build:static # pre-render configured routes to dist/static/
|
|
22
|
+
npm run serve:static # serve pre-rendered files locally
|
|
23
|
+
{{#endif}}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Architecture
|
|
27
|
+
|
|
28
|
+
Components live in `src/components/` and are [Rasti components](./AGENTS-RASTI.md) defined with `Component.create`.
|
|
29
|
+
`App` is the root component — it owns `this.state` and passes values and callbacks down as props. State is a [Rasti Model](./AGENTS-RASTI.md); mutations trigger re-renders automatically.
|
|
30
|
+
{{#if CSSFUN}}
|
|
31
|
+
Theming uses `src/theme.js` (light/dark via `data-color-scheme` on `<html>`; CSS variables prefixed `--fun-*`).
|
|
32
|
+
{{#endif}}
|
|
33
|
+
{{#if ROUTER}}
|
|
34
|
+
Routing is handled by `src/router-setup.js` — routes update `state.location`; client-side links use the `data-router` attribute.
|
|
35
|
+
{{#endif}}
|
|
36
|
+
{{#if STATIC}}
|
|
37
|
+
Routes to pre-render are listed in `static.config.js`.
|
|
38
|
+
{{#endif}}
|
|
39
|
+
|
|
40
|
+
## Conventions
|
|
41
|
+
|
|
42
|
+
- ESM throughout, 4-space indentation, single quotes, semicolons
|
|
43
|
+
- Spaces around colons in objects: `{ key : value }`
|
|
44
|
+
- Component files are PascalCase; utilities are camelCase
|
|
45
|
+
|
|
46
|
+
## Rasti API reference
|
|
47
|
+
|
|
48
|
+
See [AGENTS-RASTI.md](./AGENTS-RASTI.md) — Rasti's AGENTS.md: component creation, template interpolations, lifecycle hooks, Model.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Component, Model } from 'rasti';
|
|
2
|
+
import { css } from 'cssfun';
|
|
3
|
+
import Header from './components/Header.js';
|
|
4
|
+
import Home from './components/Home.js';
|
|
5
|
+
{{#if ROUTER}}
|
|
6
|
+
import About from './components/About.js';
|
|
7
|
+
import { createAppRouter } from './router-setup.js';
|
|
8
|
+
{{#endif}}
|
|
9
|
+
|
|
10
|
+
const { classes } = css({
|
|
11
|
+
'@global' : {
|
|
12
|
+
body : {
|
|
13
|
+
background : 'var(--fun-bg)',
|
|
14
|
+
color : 'var(--fun-text)',
|
|
15
|
+
margin : 0,
|
|
16
|
+
padding : 0
|
|
17
|
+
},
|
|
18
|
+
'a' : {
|
|
19
|
+
color : 'var(--fun-link)',
|
|
20
|
+
textDecoration : 'none',
|
|
21
|
+
transition : 'color 0.2s ease',
|
|
22
|
+
'&:hover' : { color : 'var(--fun-linkHover)', textDecoration : 'underline' }
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
root : {
|
|
26
|
+
fontFamily : 'system-ui, sans-serif',
|
|
27
|
+
minHeight : '100dvh',
|
|
28
|
+
display : 'flex',
|
|
29
|
+
flexDirection : 'column',
|
|
30
|
+
alignItems : 'center',
|
|
31
|
+
gap : 'clamp(24px, 5vw, 40px)',
|
|
32
|
+
maxWidth : '1000px',
|
|
33
|
+
margin : '0 auto',
|
|
34
|
+
padding : 'clamp(20px, 5vw, 48px)',
|
|
35
|
+
boxSizing : 'border-box'
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @typedef {Object} AppState
|
|
41
|
+
* @property {number} count
|
|
42
|
+
{{#if ROUTER}}
|
|
43
|
+
* @property {import('./router-setup.js').Location | null} location
|
|
44
|
+
{{#endif}}
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/** Root application component with CSS-in-JS global styles.
|
|
48
|
+
* @type {typeof import('rasti').Component<{}, AppState>}
|
|
49
|
+
*/
|
|
50
|
+
const App = Component.create`
|
|
51
|
+
<div class="${classes.root}">
|
|
52
|
+
<${Header} />
|
|
53
|
+
{{#if ROUTER}}
|
|
54
|
+
${({ state, partial }) => state.location?.test('/') ?
|
|
55
|
+
partial`<${Home} count="${({ state }) => state.count}" handleIncrement="${({ state }) => () => { state.count++; }}" />` :
|
|
56
|
+
partial`<${About} />`}
|
|
57
|
+
{{#else}}
|
|
58
|
+
<${Home} count="${({ state }) => state.count}" handleIncrement="${({ state }) => () => { state.count++; }}" />
|
|
59
|
+
{{#endif}}
|
|
60
|
+
</div>
|
|
61
|
+
{{#if ROUTER}}
|
|
62
|
+
`.extend({
|
|
63
|
+
/**
|
|
64
|
+
* @param {Object} [options]
|
|
65
|
+
* @param {string} [options.url] - Initial URL for server-side routing.
|
|
66
|
+
*/
|
|
67
|
+
onCreate(options = {}) {
|
|
68
|
+
this.state = new Model({ location : null, count : 0 });
|
|
69
|
+
this.router = createAppRouter(this.state);
|
|
70
|
+
const url = options.url ?? (typeof window !== 'undefined' ? window.location.pathname + window.location.search : '/');
|
|
71
|
+
this.router.navigate(url, { addToHistory : false });
|
|
72
|
+
},
|
|
73
|
+
onHydrate() {
|
|
74
|
+
this.destroyQueue.push(
|
|
75
|
+
this.router.delegateNavigation(this.el),
|
|
76
|
+
this.router.bindHistory()
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
{{#else}}
|
|
81
|
+
`.extend({
|
|
82
|
+
onCreate(options = {}) {
|
|
83
|
+
this.state = new Model({ count : 0 });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
{{#endif}}
|
|
87
|
+
|
|
88
|
+
export default App;
|