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,43 @@
|
|
|
1
|
+
import { Component } from 'rasti';
|
|
2
|
+
import { css } from 'cssfun';
|
|
3
|
+
|
|
4
|
+
const { classes } = css({
|
|
5
|
+
root : {
|
|
6
|
+
width : '100%',
|
|
7
|
+
display : 'flex',
|
|
8
|
+
justifyContent : 'center'
|
|
9
|
+
},
|
|
10
|
+
info : {
|
|
11
|
+
width : 'min(100%, 720px)',
|
|
12
|
+
padding : 'clamp(20px, 4vw, 30px)',
|
|
13
|
+
background : 'var(--fun-cardBg)',
|
|
14
|
+
borderRadius : '24px',
|
|
15
|
+
border : '1px solid var(--fun-cardBorder)',
|
|
16
|
+
boxShadow : 'var(--fun-cardInset), var(--fun-cardShadow)',
|
|
17
|
+
backdropFilter : 'blur(18px)'
|
|
18
|
+
},
|
|
19
|
+
infoStack : {
|
|
20
|
+
display : 'grid',
|
|
21
|
+
gap : '16px'
|
|
22
|
+
},
|
|
23
|
+
infoDescription : {
|
|
24
|
+
margin : 0,
|
|
25
|
+
lineHeight : '1.7',
|
|
26
|
+
fontSize : '15px'
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* About page (cssfun).
|
|
32
|
+
*/
|
|
33
|
+
const About = Component.create`
|
|
34
|
+
<div class="${classes.root}">
|
|
35
|
+
<div class="${classes.info}">
|
|
36
|
+
<div class="${classes.infoStack}">
|
|
37
|
+
<p class="${classes.infoDescription}">This is the About page. Add your content here.</p>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
export default About;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Component } from 'rasti';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* About page.
|
|
5
|
+
*/
|
|
6
|
+
const About = Component.create`
|
|
7
|
+
<div class="flex w-full justify-center">
|
|
8
|
+
<div class="w-full max-w-[720px] rounded-3xl border border-white/12 bg-linear-to-b from-[rgba(20,24,28,0.92)] to-[rgba(10,10,10,0.82)] p-5 md:p-[30px] shadow-[inset_0_1px_0_rgba(255,255,255,0.06),0_12px_40px_rgba(0,0,0,0.2)] backdrop-blur-[18px]">
|
|
9
|
+
<p class="m-0 text-[15px] leading-[1.7] text-white/88">This is the About page. Add your content here.</p>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
export default About;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Component } from 'rasti';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* About page.
|
|
5
|
+
*/
|
|
6
|
+
const About = Component.create`
|
|
7
|
+
<div class="page">
|
|
8
|
+
<div class="info">
|
|
9
|
+
<div class="info-stack">
|
|
10
|
+
<p class="info-description">This is the About page. Add your content here.</p>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
export default About;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import createRouter from './lib/router.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_TITLE = '{{NAME}}';
|
|
4
|
+
|
|
5
|
+
/** Maps route paths to document titles. */
|
|
6
|
+
const PAGE_TITLES = {
|
|
7
|
+
'/' : 'Home | {{NAME}}',
|
|
8
|
+
'/about' : 'About | {{NAME}}'
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** Normalize path for lookup (strip trailing slash; '' -> '/'). */
|
|
12
|
+
function normalizePath(p) {
|
|
13
|
+
return (p || '/').replace(/\/$/, '') || '/';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get document title for a route path.
|
|
18
|
+
* @param {string} path - Route path (e.g. '/', '/about', '/about/')
|
|
19
|
+
* @returns {string} Page title
|
|
20
|
+
*/
|
|
21
|
+
export function getTitleForPath(path) {
|
|
22
|
+
return PAGE_TITLES[normalizePath(path)] ?? DEFAULT_TITLE;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get document title for a request URL (for SSR).
|
|
27
|
+
* @param {string} url - Request URL
|
|
28
|
+
* @returns {string} Page title
|
|
29
|
+
*/
|
|
30
|
+
export function getTitleForUrl(url) {
|
|
31
|
+
const pathname = typeof url === 'string' ? url.replace(/\?.*$/, '').replace(/^https?:\/\/[^/]+/, '') || '/' : '/';
|
|
32
|
+
const path = pathname === '' ? '/' : pathname;
|
|
33
|
+
return getTitleForPath(path);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create router bound to model (sets model.location on navigate).
|
|
38
|
+
* Updates document.title on client when route changes.
|
|
39
|
+
* @param {object} model - Model with location
|
|
40
|
+
* @returns {object} Router with navigate, delegateNavigation, bindHistory, createUrl
|
|
41
|
+
*/
|
|
42
|
+
export function createAppRouter(model) {
|
|
43
|
+
const routes = [
|
|
44
|
+
{
|
|
45
|
+
path : '/',
|
|
46
|
+
action : (location) => {
|
|
47
|
+
model.location = location;
|
|
48
|
+
if (typeof document !== 'undefined') document.title = getTitleForPath(location.path);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
path : '{{ROUTE_ABOUT}}',
|
|
53
|
+
action : (location) => {
|
|
54
|
+
model.location = location;
|
|
55
|
+
if (typeof document !== 'undefined') document.title = getTitleForPath(location.path);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
];
|
|
59
|
+
return createRouter(routes, { baseUrl : '' });
|
|
60
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="{{FAVICON_SRC}}" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>{{NAME}}</title>
|
|
8
|
+
<!--app-head-->
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="app"><!--app-html--></div>
|
|
12
|
+
<script type="module" src="/src/entry-client.js"></script>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createTheme } from 'cssfun';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Application color theme with light and dark variants.
|
|
5
|
+
* CSS custom properties are scoped under the `--fun-*` prefix (e.g. `--fun-bg`, `--fun-text`).
|
|
6
|
+
* The active scheme is controlled by the `data-color-scheme` attribute on `<html>`;
|
|
7
|
+
* it defaults to the system preference and can be toggled at runtime.
|
|
8
|
+
*/
|
|
9
|
+
export const theme = createTheme({
|
|
10
|
+
light : {
|
|
11
|
+
bg : '#f5f5f5',
|
|
12
|
+
text : '#1a1a1a',
|
|
13
|
+
accent : '#0d9488',
|
|
14
|
+
link : '#0d9488',
|
|
15
|
+
linkHover : '#14b8a6',
|
|
16
|
+
cardBg : 'linear-gradient(180deg, rgba(255, 255, 255, 0.84) 0%, rgba(255, 255, 255, 0.72) 100%)',
|
|
17
|
+
cardBorder : 'rgba(15, 23, 42, 0.1)',
|
|
18
|
+
cardShadow : '0 8px 32px rgba(15, 23, 42, 0.08)',
|
|
19
|
+
cardInset : 'inset 0 1px 0 rgba(255, 255, 255, 0.45)',
|
|
20
|
+
metaBg : 'rgba(15, 23, 42, 0.04)',
|
|
21
|
+
metaBorder : 'rgba(15, 23, 42, 0.06)',
|
|
22
|
+
buttonBg : 'linear-gradient(180deg, rgba(13, 148, 136, 0.18) 0%, rgba(255, 255, 255, 0.92) 100%)',
|
|
23
|
+
buttonBorder : 'rgba(15, 23, 42, 0.1)',
|
|
24
|
+
buttonHoverBg : 'linear-gradient(180deg, rgba(13, 148, 136, 0.26) 0%, rgba(255, 255, 255, 0.96) 100%)',
|
|
25
|
+
buttonHoverBorder : 'rgba(13, 148, 136, 0.4)',
|
|
26
|
+
buttonShadow : '0 4px 16px rgba(15, 23, 42, 0.06)',
|
|
27
|
+
buttonHoverShadow : '0 4px 16px rgba(15, 23, 42, 0.1)',
|
|
28
|
+
codeBg : 'rgba(15, 23, 42, 0.05)',
|
|
29
|
+
codeBorder : 'rgba(15, 23, 42, 0.1)',
|
|
30
|
+
mutedText : 'rgba(15, 23, 42, 0.56)',
|
|
31
|
+
showWhenLight : '',
|
|
32
|
+
showWhenDark : 'none'
|
|
33
|
+
},
|
|
34
|
+
dark : {
|
|
35
|
+
bg : '#0a0a0a',
|
|
36
|
+
text : '#ffffff',
|
|
37
|
+
accent : '#5ee9df',
|
|
38
|
+
link : '#5ee9df',
|
|
39
|
+
linkHover : '#93fdf5',
|
|
40
|
+
cardBg : 'linear-gradient(180deg, rgba(20, 24, 28, 0.92) 0%, rgba(10, 10, 10, 0.82) 100%)',
|
|
41
|
+
cardBorder : 'rgba(255, 255, 255, 0.1)',
|
|
42
|
+
cardShadow : '0 12px 40px rgba(0, 0, 0, 0.2)',
|
|
43
|
+
cardInset : 'inset 0 1px 0 rgba(255, 255, 255, 0.06)',
|
|
44
|
+
metaBg : 'rgba(255, 255, 255, 0.04)',
|
|
45
|
+
metaBorder : 'rgba(255, 255, 255, 0.06)',
|
|
46
|
+
buttonBg : 'linear-gradient(180deg, rgba(94, 233, 223, 0.2) 0%, rgba(255, 255, 255, 0.06) 100%)',
|
|
47
|
+
buttonBorder : 'rgba(255, 255, 255, 0.12)',
|
|
48
|
+
buttonHoverBg : 'linear-gradient(180deg, rgba(94, 233, 223, 0.28) 0%, rgba(255, 255, 255, 0.1) 100%)',
|
|
49
|
+
buttonHoverBorder : 'rgba(94, 233, 223, 0.4)',
|
|
50
|
+
buttonShadow : '0 6px 20px rgba(0, 0, 0, 0.16)',
|
|
51
|
+
buttonHoverShadow : '0 6px 20px rgba(0, 0, 0, 0.2)',
|
|
52
|
+
codeBg : 'rgba(255, 255, 255, 0.08)',
|
|
53
|
+
codeBorder : 'rgba(255, 255, 255, 0.1)',
|
|
54
|
+
mutedText : 'rgba(255, 255, 255, 0.56)',
|
|
55
|
+
showWhenLight : 'none',
|
|
56
|
+
showWhenDark : ''
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export default theme;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
@theme {
|
|
4
|
+
--color-app-link: #5ee9df;
|
|
5
|
+
--color-app-link-hover: #93fdf5;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
body {
|
|
9
|
+
background: #0a0a0a;
|
|
10
|
+
color: #ffffff;
|
|
11
|
+
margin: 0;
|
|
12
|
+
padding: 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
a,
|
|
16
|
+
a[data-router] {
|
|
17
|
+
color: var(--color-app-link);
|
|
18
|
+
text-decoration: none;
|
|
19
|
+
transition: color 0.2s ease;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
a:hover,
|
|
23
|
+
a[data-router]:hover {
|
|
24
|
+
color: var(--color-app-link-hover);
|
|
25
|
+
text-decoration: underline;
|
|
26
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="{{FAVICON_SRC}}" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>{{NAME}}</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app"></div>
|
|
11
|
+
<script type="module" src="/src/main.js"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
14
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name" : "rasti-app",
|
|
3
|
+
"private" : true,
|
|
4
|
+
"version" : "0.0.0",
|
|
5
|
+
"type" : "module",
|
|
6
|
+
"scripts" : {
|
|
7
|
+
"dev" : "vite",
|
|
8
|
+
"build" : "vite build",
|
|
9
|
+
"preview" : "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies" : {
|
|
12
|
+
"rasti" : "{{RASTI_VERSION}}"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies" : {
|
|
15
|
+
"vite" : "{{VITE_VERSION}}"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import App from './App.js';
|
|
2
|
+
{{#if CSSFUN}}
|
|
3
|
+
import { theme } from './theme.js';
|
|
4
|
+
|
|
5
|
+
document.body.classList.add(theme.classes.root);
|
|
6
|
+
{{#else}}
|
|
7
|
+
import './style.css';
|
|
8
|
+
{{#endif}}
|
|
9
|
+
|
|
10
|
+
{{#if ROUTER}}
|
|
11
|
+
const options = { url : window.location.pathname + window.location.search };
|
|
12
|
+
App.mount(options, document.querySelector('#app'));
|
|
13
|
+
{{#else}}
|
|
14
|
+
App.mount({}, document.querySelector('#app'));
|
|
15
|
+
{{#endif}}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
|
|
4
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
5
|
+
const base = process.env.BASE || '/';
|
|
6
|
+
|
|
7
|
+
const templateHtml = isProduction
|
|
8
|
+
? await fs.readFile('./dist/client/index.html', 'utf-8')
|
|
9
|
+
: '';
|
|
10
|
+
|
|
11
|
+
const app = express();
|
|
12
|
+
|
|
13
|
+
/** @type {object|undefined} Vite dev server instance */
|
|
14
|
+
let vite;
|
|
15
|
+
if (!isProduction) {
|
|
16
|
+
const { createServer } = await import('vite');
|
|
17
|
+
vite = await createServer({
|
|
18
|
+
server : { middlewareMode : true },
|
|
19
|
+
appType : 'custom',
|
|
20
|
+
base
|
|
21
|
+
});
|
|
22
|
+
app.use(vite.middlewares);
|
|
23
|
+
} else {
|
|
24
|
+
const compression = (await import('compression')).default;
|
|
25
|
+
const sirv = (await import('sirv')).default;
|
|
26
|
+
app.use(compression());
|
|
27
|
+
app.use(base, sirv('./dist/client', { extensions : [] }));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
app.use('*all', async (req, res) => {
|
|
31
|
+
try {
|
|
32
|
+
const url = '/' + req.originalUrl.replace(base, '');
|
|
33
|
+
|
|
34
|
+
/** @type {string} */
|
|
35
|
+
let template;
|
|
36
|
+
/** @type {Function} Server render function */
|
|
37
|
+
let render;
|
|
38
|
+
if (!isProduction) {
|
|
39
|
+
template = await fs.readFile('./index.html', 'utf-8');
|
|
40
|
+
template = await vite.transformIndexHtml(url, template);
|
|
41
|
+
render = (await vite.ssrLoadModule('/src/entry-server.js')).render;
|
|
42
|
+
} else {
|
|
43
|
+
template = templateHtml;
|
|
44
|
+
render = (await import('./dist/server/entry-server.js')).render;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const rendered = await render(url);
|
|
48
|
+
|
|
49
|
+
let html = template
|
|
50
|
+
.replace('<!--app-head-->', rendered.head ?? '')
|
|
51
|
+
.replace('<!--app-html-->', rendered.html ?? '');
|
|
52
|
+
|
|
53
|
+
if (rendered.bodyClassName) {
|
|
54
|
+
html = html.replace('<body>', '<body class="' + rendered.bodyClassName + '">');
|
|
55
|
+
}
|
|
56
|
+
if (rendered.dataColorScheme) {
|
|
57
|
+
html = html.replace('<html', '<html data-color-scheme="' + rendered.dataColorScheme + '"');
|
|
58
|
+
}
|
|
59
|
+
if (rendered.title) {
|
|
60
|
+
html = html.replace(/<title>[\s\S]*?<\/title>/, '<title>' + rendered.title + '</title>');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
res.status(200).set({ 'Content-Type' : 'text/html' }).send(html);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
vite?.ssrFixStacktrace(e);
|
|
66
|
+
console.log(e.stack);
|
|
67
|
+
res.status(500).end(e.stack);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export default app;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="{{FAVICON_SRC}}" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>{{NAME}}</title>
|
|
8
|
+
<link rel="stylesheet" href="/src/style.css" />
|
|
9
|
+
<!--app-head-->
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="app"><!--app-html--></div>
|
|
13
|
+
<script type="module" src="/src/entry-client.js"></script>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
|
16
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name" : "rasti-ssr-app",
|
|
3
|
+
"private" : true,
|
|
4
|
+
"version" : "0.0.0",
|
|
5
|
+
"type" : "module",
|
|
6
|
+
"scripts" : {
|
|
7
|
+
"dev" : "node server",
|
|
8
|
+
"build" : "npm run build:client && npm run build:server",
|
|
9
|
+
"build:client" : "vite build --outDir dist/client",
|
|
10
|
+
"build:server" : "vite build --ssr src/entry-server.js --outDir dist/server",
|
|
11
|
+
"preview" : "cross-env NODE_ENV=production node server"
|
|
12
|
+
},
|
|
13
|
+
"dependencies" : {
|
|
14
|
+
"compression" : "{{COMPRESSION_VERSION}}",
|
|
15
|
+
"express" : "{{EXPRESS_VERSION}}",
|
|
16
|
+
"rasti" : "{{RASTI_VERSION}}",
|
|
17
|
+
"sirv" : "{{SIRV_VERSION}}"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies" : {
|
|
20
|
+
"cross-env" : "{{CROSS_ENV_VERSION}}",
|
|
21
|
+
"vite" : "{{VITE_VERSION}}"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import App from './App.js';
|
|
2
|
+
{{#if CSSFUN}}
|
|
3
|
+
import { theme } from './theme.js';
|
|
4
|
+
|
|
5
|
+
document.body.classList.add(theme.classes.root);
|
|
6
|
+
{{#else}}
|
|
7
|
+
import './style.css';
|
|
8
|
+
{{#endif}}
|
|
9
|
+
|
|
10
|
+
// The third argument `true` enables hydration of server-rendered HTML.
|
|
11
|
+
{{#if ROUTER}}
|
|
12
|
+
App.mount(window.__APP_OPTIONS__, document.querySelector('#app'), true);
|
|
13
|
+
{{#else}}
|
|
14
|
+
App.mount({}, document.querySelector('#app'), true);
|
|
15
|
+
{{#endif}}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Component } from 'rasti';
|
|
2
|
+
import App from './App.js';
|
|
3
|
+
{{#if ROUTER}}
|
|
4
|
+
import { getTitleForUrl } from './router-setup.js';
|
|
5
|
+
{{#endif}}
|
|
6
|
+
{{#if CSSFUN}}
|
|
7
|
+
import { theme } from './theme.js';
|
|
8
|
+
import { StyleSheet } from 'cssfun';
|
|
9
|
+
{{#endif}}
|
|
10
|
+
|
|
11
|
+
{{#if ROUTER}}
|
|
12
|
+
/**
|
|
13
|
+
* SSR render: pass url so App onCreate runs router.navigate; inject __APP_OPTIONS__ and page title for client hydrate.
|
|
14
|
+
* @param {string} url - Request URL
|
|
15
|
+
* @returns {{ html: string, head?: string, bodyClassName?: string, title?: string }}
|
|
16
|
+
*/
|
|
17
|
+
export function render(url) {
|
|
18
|
+
Component.resetUid();
|
|
19
|
+
const options = { url };
|
|
20
|
+
const app = App.mount(options);
|
|
21
|
+
const html = app.toString();
|
|
22
|
+
const title = getTitleForUrl(url);
|
|
23
|
+
{{#if CSSFUN}}
|
|
24
|
+
const styles = StyleSheet.toString();
|
|
25
|
+
const script = `<script>window.__APP_OPTIONS__=${JSON.stringify(options)}</script>`;
|
|
26
|
+
return { html, head : styles + script, title, bodyClassName : theme.classes.root };
|
|
27
|
+
{{#else}}
|
|
28
|
+
const script = `<script>window.__APP_OPTIONS__=${JSON.stringify(options)}</script>`;
|
|
29
|
+
return { html, head : script, title };
|
|
30
|
+
{{#endif}}
|
|
31
|
+
}
|
|
32
|
+
{{#else}}
|
|
33
|
+
/**
|
|
34
|
+
* Server-side render function.
|
|
35
|
+
* Returns HTML string to be injected into the template.
|
|
36
|
+
* @param {string} _url - Request URL
|
|
37
|
+
* @returns {object} Rendered content { html: string, head?: string }
|
|
38
|
+
*/
|
|
39
|
+
export function render(_url) {
|
|
40
|
+
Component.resetUid();
|
|
41
|
+
const html = App.mount({}).toString();
|
|
42
|
+
{{#if CSSFUN}}
|
|
43
|
+
const styles = StyleSheet.toString();
|
|
44
|
+
return { html, head : styles, bodyClassName : theme.classes.root };
|
|
45
|
+
{{#else}}
|
|
46
|
+
return { html };
|
|
47
|
+
{{#endif}}
|
|
48
|
+
}
|
|
49
|
+
{{#endif}}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const projectRoot = path.resolve(__dirname, '..');
|
|
7
|
+
const distStatic = path.join(projectRoot, 'dist', 'static');
|
|
8
|
+
const distClient = path.join(projectRoot, 'dist', 'client');
|
|
9
|
+
const port = Number(process.env.PORT) || 37521;
|
|
10
|
+
|
|
11
|
+
process.env.NODE_ENV = 'production';
|
|
12
|
+
|
|
13
|
+
async function waitForServer(baseUrl) {
|
|
14
|
+
for (let i = 0; i < 30; i++) {
|
|
15
|
+
try {
|
|
16
|
+
const res = await fetch(baseUrl);
|
|
17
|
+
if (res.ok) return true;
|
|
18
|
+
} catch {
|
|
19
|
+
// keep waiting
|
|
20
|
+
}
|
|
21
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function urlToFilePath(fullUrl) {
|
|
27
|
+
const pathname = fullUrl.replace(/\?.*$/, '').replace(/^https?:\/\/[^/]+/, '') || '/';
|
|
28
|
+
const clean = pathname.replace(/^\//, '').replace(/\/$/, '') || 'index';
|
|
29
|
+
return clean === 'index' ? 'index.html' : path.join(clean, 'index.html');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Normalize a route path for fetching (trailing slash except root).
|
|
34
|
+
* @param {string} s
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
function normalizeRoutePath(s) {
|
|
38
|
+
if (s === '/' || s === '') return '/';
|
|
39
|
+
return s.endsWith('/') ? s : `${s}/`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Ensure output path stays under dist/static (no path traversal).
|
|
44
|
+
* @param {string} distRoot
|
|
45
|
+
* @param {string} rel - posix-style relative path (e.g. 404.html, nested/x.html)
|
|
46
|
+
*/
|
|
47
|
+
function assertSafeOutput(distRoot, rel) {
|
|
48
|
+
const normalized = path.normalize(rel.replace(/\//g, path.sep));
|
|
49
|
+
if (path.isAbsolute(normalized) || normalized.startsWith('..' + path.sep) || normalized === '..') {
|
|
50
|
+
throw new Error(`Invalid output (must be relative, no ".."): ${rel}`);
|
|
51
|
+
}
|
|
52
|
+
const resolved = path.resolve(distRoot, normalized);
|
|
53
|
+
const relToRoot = path.relative(distRoot, resolved);
|
|
54
|
+
if (relToRoot.startsWith('..') || path.isAbsolute(relToRoot)) {
|
|
55
|
+
throw new Error(`Invalid output path: ${rel}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {string | { route: string, output: string }} item
|
|
61
|
+
* @param {string} baseUrl
|
|
62
|
+
* @returns {{ fetchPath: string, fullUrl: string, outRelative: string }}
|
|
63
|
+
*/
|
|
64
|
+
function resolveConfigEntry(item, baseUrl) {
|
|
65
|
+
if (typeof item === 'string') {
|
|
66
|
+
const fetchPath = normalizeRoutePath(item);
|
|
67
|
+
const fullUrl = fetchPath === '/' ? `${baseUrl}/` : `${baseUrl}${fetchPath}`;
|
|
68
|
+
const outRelative = urlToFilePath(fullUrl);
|
|
69
|
+
return { fetchPath, fullUrl, outRelative };
|
|
70
|
+
}
|
|
71
|
+
if (item && typeof item === 'object' && typeof item.route === 'string' && typeof item.output === 'string') {
|
|
72
|
+
const fetchPath = normalizeRoutePath(item.route);
|
|
73
|
+
const fullUrl = fetchPath === '/' ? `${baseUrl}/` : `${baseUrl}${fetchPath}`;
|
|
74
|
+
const outRelative = item.output.replace(/\\/g, '/');
|
|
75
|
+
return { fetchPath, fullUrl, outRelative };
|
|
76
|
+
}
|
|
77
|
+
throw new Error(
|
|
78
|
+
'Each static.config.js entry must be a string route or { route: string, output: string }.'
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function main() {
|
|
83
|
+
const configPath = path.join(projectRoot, 'static.config.js');
|
|
84
|
+
let entries;
|
|
85
|
+
try {
|
|
86
|
+
const { default: config } = await import(configPath);
|
|
87
|
+
entries = Array.isArray(config) ? config : ['/'];
|
|
88
|
+
} catch (e) {
|
|
89
|
+
console.error(
|
|
90
|
+
'Missing or invalid static.config.js. Export an array of routes (strings) and/or { route, output } objects.'
|
|
91
|
+
);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const base = process.env.BASE || '/';
|
|
96
|
+
const baseUrl = `http://localhost:${port}${base.replace(/\/$/, '') || ''}`;
|
|
97
|
+
|
|
98
|
+
await fs.rm(distStatic, { recursive : true }).catch(() => {});
|
|
99
|
+
await fs.mkdir(distStatic, { recursive : true });
|
|
100
|
+
await fs.cp(distClient, distStatic, { recursive : true });
|
|
101
|
+
|
|
102
|
+
// Ensure public dir contents are at the root of dist/static (Vite already copies them to dist/client, but we merge again so it works even with custom publicDir)
|
|
103
|
+
const publicDir = path.join(projectRoot, 'public');
|
|
104
|
+
try {
|
|
105
|
+
const publicEntries = await fs.readdir(publicDir, { withFileTypes : true });
|
|
106
|
+
for (const e of publicEntries) {
|
|
107
|
+
const src = path.join(publicDir, e.name);
|
|
108
|
+
const dest = path.join(distStatic, e.name);
|
|
109
|
+
if (e.isDirectory()) {
|
|
110
|
+
await fs.cp(src, dest, { recursive : true });
|
|
111
|
+
} else {
|
|
112
|
+
await fs.copyFile(src, dest);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
// no public dir or empty
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const appPath = path.join(projectRoot, 'app.js');
|
|
120
|
+
const { default: app } = await import(pathToFileURL(appPath).href);
|
|
121
|
+
const server = app.listen(port);
|
|
122
|
+
|
|
123
|
+
const ready = await waitForServer(baseUrl + '/');
|
|
124
|
+
if (!ready) {
|
|
125
|
+
server.close();
|
|
126
|
+
console.error('Server did not start in time.');
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let resolved;
|
|
131
|
+
try {
|
|
132
|
+
resolved = entries.map((item) => resolveConfigEntry(item, baseUrl));
|
|
133
|
+
} catch (err) {
|
|
134
|
+
server.close();
|
|
135
|
+
console.error(err.message || err);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const { fetchPath, fullUrl, outRelative } of resolved) {
|
|
140
|
+
assertSafeOutput(distStatic, outRelative);
|
|
141
|
+
const res = await fetch(fullUrl);
|
|
142
|
+
if (!res.ok) {
|
|
143
|
+
server.close();
|
|
144
|
+
console.error(`Failed to fetch ${fullUrl}: ${res.status}`);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
const html = await res.text();
|
|
148
|
+
const filePath = path.join(distStatic, outRelative);
|
|
149
|
+
await fs.mkdir(path.dirname(filePath), { recursive : true });
|
|
150
|
+
await fs.writeFile(filePath, html, 'utf-8');
|
|
151
|
+
console.log(` ${fetchPath} -> ${outRelative}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
server.close();
|
|
155
|
+
console.log('Static build done.');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
main().catch((e) => {
|
|
159
|
+
console.error(e);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
});
|