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
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,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,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,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)
|