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
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Alberto Masuelli
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # create-rasti
2
+
3
+ Scaffold [Rasti](https://rasti.js.org) + [Vite](https://vite.dev) projects from the command line.
4
+
5
+ Pick a template (SPA, SSR, or pre-rendered Static), add styling, routing, and icon components — get a ready-to-run project in seconds.
6
+
7
+ ## Quick start
8
+
9
+ ```bash
10
+ npm create rasti # interactive
11
+ npm create rasti my-app # SPA, non-interactive
12
+ ```
13
+
14
+ Create a server-rendered project with routing and Tailwind:
15
+
16
+ ```bash
17
+ npm create rasti my-app --ssr --router --tailwind
18
+ ```
19
+
20
+ ## Templates
21
+
22
+ - **SPA** *(default)*: Rasti + Vite for client-rendered applications.
23
+ - **SSR** *(`--ssr`)*: Rasti + Vite + Express, rendered on the server and hydrated in the browser.
24
+ - **Static** *(`--static`)*: SSR in development, plus `npm run build:static` to pre-render the URLs declared in `static.config.js` into `dist/static`.
25
+
26
+ For Static projects, string entries derive the output path from the route (`/about/` becomes `about/index.html`). Object entries such as `{ route, output }` allow custom output files, including a top-level `404.html`.
27
+
28
+ ## Styling
29
+
30
+ Styling options are mutually exclusive:
31
+
32
+ - **`--tailwind`**: add [Tailwind CSS](https://tailwindcss.com).
33
+ - **`--cssfun`**: add [CSSFUN](https://cssfun.js.org), including light and dark theme setup.
34
+
35
+ Without either flag, the generated project uses plain CSS.
36
+
37
+ ## Extras
38
+
39
+ ### `--router`
40
+
41
+ Adds a small universal router built on [path-to-regexp](https://github.com/pillarjs/path-to-regexp). It is copied into the generated project as `src/lib/router.js`, then wired through `src/router-setup.js`, example pages, and an App shell with navigation.
42
+
43
+ The router is intentionally narrow: route matching, URL creation, browser history, and delegated `a[data-router]` links. It works with SPA, SSR, and Static templates.
44
+
45
+ ### `--icons [preset[,preset...]]`
46
+
47
+ Generates one Rasti component per SVG icon under `src/icons/`:
48
+
49
+ ```js
50
+ import Heart from './icons/Heart.js';
51
+ // <Heart className="..." width={24} height={24} />
52
+ ```
53
+
54
+ A single preset writes components directly to `src/icons/`. Multiple presets write each set under `src/icons/<preset>/`.
55
+
56
+ | Preset | Source | License |
57
+ |---|---|---|
58
+ | `heroicons-outline` *(default)* | [Heroicons](https://heroicons.com) by Tailwind Labs | MIT |
59
+ | `heroicons-solid` | [Heroicons](https://heroicons.com) by Tailwind Labs | MIT |
60
+ | `akar-icons` | [Akar Icons](https://akaricons.com) by Arturo Wibawa | MIT |
61
+ | `feathericon` | [Feathericon](https://feathericon.github.io/feathericon/) by Megumi Hano | MIT |
62
+ | `pixelarticons` | [Pixelarticons](https://pixelarticons.com) by Halfmage | MIT |
63
+
64
+ SVGs are downloaded from GitHub when the project is generated, so this option requires an internet connection.
65
+
66
+ The CLI writes an `AGENTS.md` to the generated project root with project-specific context (stack, commands, architecture, conventions). It also tries to fetch Rasti's `AGENTS.md` as `AGENTS-RASTI.md`; if that request fails, project generation continues without it.
67
+
68
+ ## Flags
69
+
70
+ | Flag | Description |
71
+ |------|-------------|
72
+ | `--ssr` | Use the SSR template |
73
+ | `--static` | Use the Static template |
74
+ | `--tailwind` | Add Tailwind CSS |
75
+ | `--cssfun` | Add CSSFUN |
76
+ | `--router` | Add micro-router |
77
+ | `--icons [preset[,preset]]` | Add rasti-icons (default preset: `heroicons-outline`) |
78
+ | `--help` | Show help |
79
+
80
+ `--ssr`/`--static` and `--tailwind`/`--cssfun` are mutually exclusive.
81
+
82
+ ## Development
83
+
84
+ ```bash
85
+ git clone https://github.com/8tentaculos/create-rasti.git
86
+ cd create-rasti
87
+ npm install
88
+ npm run test
89
+ npx create-rasti
90
+ ```
91
+
92
+ Before bumping any version pinned in generated projects, see [`docs/VERSIONS.md`](docs/VERSIONS.md).
93
+
94
+ ## License
95
+
96
+ [MIT](LICENSE)
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { run } from '../src/cli.js';
3
+
4
+ run(process.argv.slice(2));
@@ -0,0 +1,57 @@
1
+ # cn
2
+
3
+ A tiny helper that joins conditional class names into a single string, for [Rasti](https://rasti.js.org) + Vite projects. It accepts strings, numbers, nested arrays, and objects whose keys are kept when their value is truthy.
4
+
5
+ ## Usage
6
+
7
+ ```javascript
8
+ import cn from './src/index.js';
9
+
10
+ cn('a', 'b'); // => 'a b'
11
+ cn('a', { b : true, c : false }); // => 'a b'
12
+ cn('a', ['b', ['c']]); // => 'a b c'
13
+ cn('base', isActive && 'active'); // conditional, falsy values are skipped
14
+ ```
15
+
16
+ In a Rasti component:
17
+
18
+ ```javascript
19
+ import cn from '../lib/cn.js';
20
+
21
+ const Button = Component.create`
22
+ <button class="${({ props }) => cn('button', props.className)}">
23
+ ${({ props }) => props.renderChildren()}
24
+ </button>
25
+ `;
26
+ ```
27
+
28
+ ## API
29
+
30
+ `cn(...args)` returns a space-separated `string`.
31
+
32
+ Each argument may be:
33
+
34
+ | Type | Behavior |
35
+ |---|---|
36
+ | `string` / `number` | Added as-is when truthy. |
37
+ | `Array` | Flattened recursively (nesting supported). |
38
+ | `Object` | Each key is added when its value is truthy. |
39
+ | falsy (`null`, `undefined`, `false`, `0`, `''`) | Skipped. |
40
+
41
+ ## Copy model
42
+
43
+ This package is meant to be copied into an application, not installed as a public runtime dependency. `create-rasti` copies `src/index.js` into every generated project as `src/lib/cn.js`.
44
+
45
+ For manual use:
46
+
47
+ ```bash
48
+ cp extras/cn/src/index.js ./my-project/src/lib/cn.js
49
+ ```
50
+
51
+ ```javascript
52
+ import cn from './src/lib/cn.js';
53
+ ```
54
+
55
+ ## License
56
+
57
+ [MIT](../../LICENSE)
@@ -0,0 +1,9 @@
1
+ {
2
+ "name" : "cn",
3
+ "version" : "0.0.0",
4
+ "private" : true,
5
+ "description" : "Fast conditional class names helper",
6
+ "type" : "module",
7
+ "main" : "src/index.js",
8
+ "license" : "MIT"
9
+ }
@@ -0,0 +1,37 @@
1
+ const add = (a, b) => a && b ? a + ' ' + b : '' + (a || b);
2
+
3
+ const parse = (classes) => {
4
+ let out = '';
5
+
6
+ for (const c of classes) {
7
+ if (!c) continue;
8
+
9
+ const t = typeof c;
10
+
11
+ if (t === 'string' || t === 'number') out = add(out, c);
12
+ else if (Array.isArray(c)) out = add(out, parse(c));
13
+ else if (t === 'object') for (const key in c) c[key] && (out = add(out, key));
14
+ }
15
+
16
+ return out;
17
+ };
18
+
19
+ /**
20
+ * `cn` — fast conditional class names.
21
+ *
22
+ * Joins truthy class name arguments into a single space-separated string.
23
+ * Accepts strings, numbers, arrays (nested supported) and objects whose keys
24
+ * are included when their value is truthy. Falsy values are skipped.
25
+ *
26
+ * @param {...(string|number|Array|Object|null|undefined|boolean)} args - Class name values to join.
27
+ * @returns {string} The merged class name string.
28
+ *
29
+ * @example
30
+ * cn('a', 'b'); // => 'a b'
31
+ * cn('a', { b : true, c : false }); // => 'a b'
32
+ * cn('a', ['b', ['c']]); // => 'a b c'
33
+ * cn('a', isActive && 'active'); // conditional
34
+ */
35
+ const cn = (...args) => parse(args);
36
+
37
+ export default cn;
@@ -0,0 +1,78 @@
1
+ # micro-router
2
+
3
+ A minimal router for [Rasti](https://rasti.js.org) + Vite projects. It handles route matching, URL creation, browser history, and delegated link clicks. Matching is powered by [path-to-regexp](https://github.com/pillarjs/path-to-regexp).
4
+
5
+ ## Why it exists
6
+
7
+ [Rasti](https://rasti.js.org) projects need routing that runs the same way in the browser and during SSR. This router stays small for that use case: no framework adapters, no component API, and no rendering opinion. It was written for `create-rasti`, but it only depends on plain JavaScript route objects and actions.
8
+
9
+ ## Usage
10
+
11
+ ```javascript
12
+ import createRouter from './src/index.js';
13
+
14
+ const routes = [
15
+ { path : '/', action : (location) => { /* render Home */ } },
16
+ { path : '/about', action : (location) => { /* render About */ } },
17
+ { path : '/users/:id', action : (location) => { /* render User, location.params.id */ } }
18
+ ];
19
+
20
+ const router = createRouter(routes, { baseUrl : '' });
21
+
22
+ // Browser: delegate link clicks and bind history.
23
+ router.delegateNavigation(document.querySelector('#app'));
24
+ router.bindHistory();
25
+
26
+ // Programmatic navigation.
27
+ router.navigate('/about');
28
+
29
+ // Async actions can be awaited.
30
+ await router.navigate('/users/123');
31
+
32
+ // URL building with params and query.
33
+ router.createUrl('/users/:id', { id : '123' }, { tab : 'posts' });
34
+ // => '/users/123?tab=posts'
35
+ ```
36
+
37
+ In the browser, links opt in to client-side navigation with `data-router`:
38
+
39
+ ```html
40
+ <a href="/about" data-router>About</a>
41
+ ```
42
+
43
+ ## API
44
+
45
+ `createRouter(routes, options?)` returns `{ navigate, createUrl, delegateNavigation, bindHistory }`.
46
+
47
+ | Method | Description |
48
+ |---|---|
49
+ | `navigate(url, options?)` | Matches `url`, updates browser history when available, invokes the matched route's `action(location)`, and returns the action result. Async actions return a Promise. Options: `{ addToHistory = true, replaceHistory = false }`. |
50
+ | `createUrl(path, params?, query?)` | Builds a URL from a route path, route params, and query params. |
51
+ | `delegateNavigation(rootElement)` | Intercepts left-clicks on descendant `a[data-router]` links and routes them through `navigate`. Returns a cleanup function. |
52
+ | `bindHistory()` | Listens for `popstate` and navigates to the current URL without pushing a new history entry. Returns a cleanup function. |
53
+
54
+ **Options:** `{ baseUrl?: string }` — prepended to generated URLs and stripped before matching.
55
+
56
+ **Route:** `{ path: string, action: (location) => any }`.
57
+
58
+ **Location:** `{ path, params, query, test(url): boolean }` — `params` and `query` are plain objects; `test(url)` reports whether a URL matches the same route.
59
+
60
+ Full type signatures live in the JSDoc of `src/index.js`.
61
+
62
+ ## Copy model
63
+
64
+ This package is meant to be copied into an application, not installed as a public runtime dependency. `create-rasti --router` copies `src/index.js` into the generated project as `src/lib/router.js` and adds `path-to-regexp` to that project's dependencies.
65
+
66
+ For manual use:
67
+
68
+ ```bash
69
+ cp -r extras/micro-router/src ./my-project/src/router
70
+ ```
71
+
72
+ ```javascript
73
+ import createRouter from './src/router/index.js';
74
+ ```
75
+
76
+ ## License
77
+
78
+ [MIT](../../LICENSE)
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "micro-router",
3
+ "version": "0.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "micro-router",
9
+ "version": "0.0.0",
10
+ "license": "MIT",
11
+ "dependencies": {
12
+ "path-to-regexp": "^8.0.0"
13
+ }
14
+ },
15
+ "node_modules/path-to-regexp": {
16
+ "version": "8.3.0",
17
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
18
+ "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
19
+ "license": "MIT",
20
+ "funding": {
21
+ "type": "opencollective",
22
+ "url": "https://opencollective.com/express"
23
+ }
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name" : "micro-router",
3
+ "version" : "0.0.0",
4
+ "private" : true,
5
+ "description" : "Minimal universal router",
6
+ "type" : "module",
7
+ "main" : "src/index.js",
8
+ "dependencies" : {
9
+ "path-to-regexp" : "^8.0.0"
10
+ },
11
+ "license" : "MIT"
12
+ }
@@ -0,0 +1,192 @@
1
+ import { match, compile } from 'path-to-regexp';
2
+
3
+ /**
4
+ * @typedef {Object} Location
5
+ * @property {string} path - The route path pattern (e.g., `/user/:id`).
6
+ * @property {Object.<string, string>} params - The route parameters extracted from the URL path, sanitized to prevent injection attacks.
7
+ * @property {Object.<string, string>} query - The query parameters parsed from the URL query string.
8
+ * @property {function(string): boolean} test - A function to test if a given URL matches this route. Returns `true` if the URL matches the route, `false` otherwise.
9
+ */
10
+
11
+ /**
12
+ * @typedef {Object} Route
13
+ * @property {string} path - The route path pattern (e.g., `/user/:id`).
14
+ * @property {function(Location): *} action - The action function to call when the route matches. Receives a `Location` object as parameter.
15
+ */
16
+
17
+ /**
18
+ * @typedef {Object} Router
19
+ * @property {function(string, Object=): *} navigate - Navigates to a given URL, matches a route, and returns its action result.
20
+ * @property {function(string, Object=, Object=): string} createUrl - Creates a URL from a route path, parameters, and query string.
21
+ * @property {function(HTMLElement): function(): void} delegateNavigation - Delegates navigation events for links with `data-router` attributes. Returns a cleanup function.
22
+ * @property {function(): function(): void} bindHistory - Binds browser history changes to the navigation logic. Returns a cleanup function.
23
+ */
24
+
25
+ /**
26
+ * Creates a router with navigation, URL creation, and event delegation.
27
+ * @param {Array.<Route>} routes - Array of route objects. Each route must have a `path` and an `action` function.
28
+ * @param {Object} [routerOptions={}] - Options for the router.
29
+ * @param {string} [routerOptions.baseUrl=''] - A base URL to prepend to all routes and navigation.
30
+ * @returns {Router} - Router object with navigation, URL creation, and event delegation methods.
31
+ */
32
+ export default function createRouter(routes, routerOptions = {}) {
33
+ const { baseUrl = '' } = routerOptions;
34
+
35
+ /**
36
+ * Gets the pathname and query string from a given url.
37
+ * @param {string} url - The url to get the pathname and query string from.
38
+ * @returns {Object} - The pathname and query string.
39
+ * @private
40
+ */
41
+ const getUrlParts = (url) => {
42
+ const [pathname, query] = url.replace(baseUrl, '').split('?');
43
+ return { pathname, query };
44
+ };
45
+
46
+ /**
47
+ * Gets the match result from a given pathname and route path.
48
+ * @param {string} pathname - The pathname to match.
49
+ * @param {string} routePath - The route path to match.
50
+ * @returns {Object|null} - The match result, or `null` if no match is found.
51
+ * @private
52
+ */
53
+ const getMatch = (pathname, routePath) => {
54
+ const matcher = match(routePath, { decode : decodeURIComponent });
55
+ return matcher(pathname);
56
+ };
57
+
58
+ /**
59
+ * Matches a given url against the routes.
60
+ * @param {string} url - The url to match.
61
+ * @returns {(function(): *)|null} - A function that calls the matched route's action with a `Location` object, or `null` if no match is found.
62
+ * @private
63
+ */
64
+ const matchRoute = (url) => {
65
+ const { pathname, query } = getUrlParts(url);
66
+
67
+ for (const route of routes) {
68
+ const matched = getMatch(pathname, route.path);
69
+
70
+ if (matched) {
71
+ /** @type {Location} */
72
+ const location = {
73
+ path : route.path,
74
+ params : sanitizeParams(matched.params),
75
+ query : Object.fromEntries(new URLSearchParams(query).entries()),
76
+ test : (url) => !!getMatch(getUrlParts(url).pathname, route.path)
77
+ };
78
+
79
+ return () => route.action(location);
80
+ }
81
+ }
82
+
83
+ return null;
84
+ };
85
+
86
+ /**
87
+ * Navigates to a given path, matches a route, and calls its action with a `Location` object.
88
+ * @param {string} url - The path to navigate to, including query string.
89
+ * @param {Object} [options={}] - Options for navigation.
90
+ * @param {boolean} [options.addToHistory=true] - Whether to add the navigation to the browser's history.
91
+ * @param {boolean} [options.replaceHistory=false] - Whether to replace the current history entry instead of adding a new one.
92
+ * @returns {*} The matched route action result, or `undefined` if no route matches. Async actions return a Promise.
93
+ */
94
+ const navigate = (url, options = {}) => {
95
+ const { addToHistory = true, replaceHistory = false } = options;
96
+ // Match the route and query
97
+ const matched = matchRoute(url);
98
+
99
+ if (matched) {
100
+ // Handle browser history
101
+ if (addToHistory && typeof window !== 'undefined') {
102
+ window.history[replaceHistory ? 'replaceState' : 'pushState']({}, '', url);
103
+ }
104
+ // Call the route's action
105
+ return matched();
106
+ } else {
107
+ console.error('No route matched:', url);
108
+ }
109
+
110
+ return undefined;
111
+ };
112
+
113
+ /**
114
+ * Creates a URL from a route path, parameters, and query string.
115
+ * @param {string} path - The route path (e.g., `/user/:id`).
116
+ * @param {Object.<string, string>} [params={}] - Parameters to replace in the path (e.g., `{ id : '123' }` for `/user/:id`).
117
+ * @param {Object.<string, string>} [query={}] - Query parameters to append to the URL (e.g., `{ page : '1', sort : 'name' }`).
118
+ * @returns {string} - The generated URL with query parameters.
119
+ */
120
+ const createUrl = (path, params = {}, query = {}) => {
121
+ const toPath = compile(path, { encode : encodeURIComponent });
122
+ const basePath = toPath(params);
123
+
124
+ const queryString = new URLSearchParams(query).toString();
125
+ return `${baseUrl}${queryString ? `${basePath}?${queryString}` : basePath}`;
126
+ };
127
+
128
+ /**
129
+ * Delegates navigation events for links with `data-router` attributes.
130
+ * @param {HTMLElement} rootElement - The root element to listen for click events.
131
+ * @returns {Function} - A function to undelegate the event listener.
132
+ */
133
+ const delegateNavigation = (rootElement) => {
134
+ const handleClick = (event) => {
135
+ if (
136
+ event.defaultPrevented ||
137
+ event.button !== 0 || // Only left-click
138
+ event.metaKey || event.ctrlKey || event.shiftKey || event.altKey // Modifier keys
139
+ ) {
140
+ return;
141
+ }
142
+
143
+ const anchor = event.target.closest('a[data-router]');
144
+
145
+ if (anchor && anchor.href) {
146
+ event.preventDefault();
147
+ const url = new URL(anchor.href);
148
+ window.scrollTo({ top : 0, behavior : 'instant' });
149
+ navigate(url.pathname + url.search);
150
+ }
151
+ };
152
+ // Bind the click event to the root element.
153
+ rootElement.addEventListener('click', handleClick);
154
+ // Return a function to remove the event listener.
155
+ return () => {
156
+ rootElement.removeEventListener('click', handleClick);
157
+ };
158
+ };
159
+
160
+ /**
161
+ * Binds browser history changes to the navigation logic.
162
+ * @returns {Function} - A function to unbind the `popstate` event listener.
163
+ */
164
+ const bindHistory = () => {
165
+ const handlePopState = () => {
166
+ // On popstate, navigate to the current URL
167
+ navigate(window.location.pathname + window.location.search, { addToHistory : false });
168
+ };
169
+
170
+ window.addEventListener('popstate', handlePopState);
171
+
172
+ // Return a function to remove the event listener
173
+ return () => {
174
+ window.removeEventListener('popstate', handlePopState);
175
+ };
176
+ };
177
+
178
+ /**
179
+ * Sanitizes route parameters to prevent injection attacks.
180
+ * @param {Object} params - The route parameters.
181
+ * @returns {Object} - The sanitized parameters.
182
+ */
183
+ const sanitizeParams = (params) => {
184
+ const sanitized = {};
185
+ for (const [key, value] of Object.entries(params)) {
186
+ sanitized[key] = String(value).replace(/[<>]/g, '');
187
+ }
188
+ return sanitized;
189
+ };
190
+
191
+ return { navigate, createUrl, delegateNavigation, bindHistory };
192
+ }
@@ -0,0 +1,65 @@
1
+ # rasti-icons
2
+
3
+ Generate [Rasti](https://rasti.js.org) components from GitHub-hosted SVG icon sets.
4
+
5
+ This tool is used by `create-rasti --icons`, and it can also run directly when a project needs to regenerate icons or use a custom SVG source.
6
+
7
+ ## With create-rasti
8
+
9
+ ```bash
10
+ npm create rasti my-app --icons
11
+ npm create rasti my-app --icons pixelarticons
12
+ npm create rasti my-app --icons heroicons-outline,heroicons-solid
13
+ ```
14
+
15
+ The default preset is `heroicons-outline`. A single preset writes components to `src/icons/`; multiple presets write each set to `src/icons/<preset>/`.
16
+
17
+ ## Standalone usage
18
+
19
+ ### Built-in presets
20
+
21
+ ```bash
22
+ node bin/rasti-icons.js --preset heroicons-outline
23
+ node bin/rasti-icons.js --preset akar-icons --output ./src/icons
24
+ node bin/rasti-icons.js --preset feathericon --output ./src/icons
25
+ node bin/rasti-icons.js --preset pixelarticons --output ./src/icons
26
+ ```
27
+
28
+ Available presets: `heroicons-outline`, `heroicons-solid`, `akar-icons`, `feathericon`, `pixelarticons`.
29
+
30
+ When `--output` is omitted, preset output defaults to `./icons/<preset>`.
31
+
32
+ ### Custom SVG set
33
+
34
+ ```bash
35
+ node bin/rasti-icons.js \
36
+ --source https://api.github.com/repos/owner/repo/contents/path/to/svgs \
37
+ --license https://raw.githubusercontent.com/owner/repo/main/LICENSE \
38
+ --output ./icons
39
+ ```
40
+
41
+ `--source` must be a GitHub API directory listing URL that returns a JSON array of file objects with `download_url`. `--license` is optional. Custom source output defaults to `./icons`.
42
+
43
+ ## Generated components
44
+
45
+ Each SVG file becomes a Rasti component. The generated component accepts optional `props.className`, `props.width`, and `props.height`. If width or height is not provided, the original SVG dimensions are used.
46
+
47
+ ```javascript
48
+ import Heart from './icons/Heart.js';
49
+
50
+ // Mount with default size.
51
+ Heart.mount({}, container);
52
+
53
+ // Mount with custom class and size.
54
+ Heart.mount({ className : 'icon icon-red', width : '32', height : '32' }, container);
55
+ ```
56
+
57
+ Icon filenames are converted from `kebab-case.svg` to `PascalCase.js`.
58
+
59
+ ## Licenses
60
+
61
+ Each generated output directory contains a `LICENSE.txt` with the upstream icon set license when a license URL is provided.
62
+
63
+ ## License
64
+
65
+ [MIT](../../LICENSE)