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,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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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;