@beatzball/create-litro 0.1.0
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/CHANGELOG.md +19 -0
- package/LICENSE +185 -0
- package/README.md +89 -0
- package/dist/recipes/11ty-blog/recipe.config.d.ts +4 -0
- package/dist/recipes/11ty-blog/recipe.config.d.ts.map +1 -0
- package/dist/recipes/11ty-blog/recipe.config.js +9 -0
- package/dist/recipes/11ty-blog/recipe.config.js.map +1 -0
- package/dist/recipes/11ty-blog/recipe.config.ts +11 -0
- package/dist/recipes/11ty-blog/template/app.ts +18 -0
- package/dist/recipes/11ty-blog/template/content/_data/metadata.js +10 -0
- package/dist/recipes/11ty-blog/template/content/blog/blog.11tydata.json +1 -0
- package/dist/recipes/11ty-blog/template/content/blog/getting-started/index.md +66 -0
- package/dist/recipes/11ty-blog/template/content/blog/hello-world.md +39 -0
- package/dist/recipes/11ty-blog/template/litro.recipe.json +7 -0
- package/dist/recipes/11ty-blog/template/nitro.config.ts +60 -0
- package/dist/recipes/11ty-blog/template/package.json +26 -0
- package/dist/recipes/11ty-blog/template/pages/blog/[slug].ts +65 -0
- package/dist/recipes/11ty-blog/template/pages/blog/index.ts +43 -0
- package/dist/recipes/11ty-blog/template/pages/index.ts +58 -0
- package/dist/recipes/11ty-blog/template/pages/tags/[tag].ts +53 -0
- package/dist/recipes/11ty-blog/template/public/.gitkeep +0 -0
- package/dist/recipes/11ty-blog/template/server/api/hello.ts +6 -0
- package/dist/recipes/11ty-blog/template/server/api/posts.ts +8 -0
- package/dist/recipes/11ty-blog/template/server/middleware/vite-dev.ts +29 -0
- package/dist/recipes/11ty-blog/template/server/routes/[...].ts +55 -0
- package/dist/recipes/11ty-blog/template/tsconfig.json +14 -0
- package/dist/recipes/11ty-blog/template/vite.config.ts +19 -0
- package/dist/recipes/fullstack/recipe.config.d.ts +4 -0
- package/dist/recipes/fullstack/recipe.config.d.ts.map +1 -0
- package/dist/recipes/fullstack/recipe.config.js +8 -0
- package/dist/recipes/fullstack/recipe.config.js.map +1 -0
- package/dist/recipes/fullstack/recipe.config.ts +10 -0
- package/dist/recipes/fullstack/template/app.ts +20 -0
- package/dist/recipes/fullstack/template/nitro.config.ts +67 -0
- package/dist/recipes/fullstack/template/package.json +26 -0
- package/dist/recipes/fullstack/template/pages/blog/[slug].ts +43 -0
- package/dist/recipes/fullstack/template/pages/blog/index.ts +22 -0
- package/dist/recipes/fullstack/template/pages/index.ts +43 -0
- package/dist/recipes/fullstack/template/public/.gitkeep +0 -0
- package/dist/recipes/fullstack/template/server/api/hello.ts +6 -0
- package/dist/recipes/fullstack/template/server/middleware/vite-dev.ts +31 -0
- package/dist/recipes/fullstack/template/server/routes/[...].ts +59 -0
- package/dist/recipes/fullstack/template/tsconfig.json +14 -0
- package/dist/recipes/fullstack/template/vite.config.ts +15 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +200 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/scaffold.d.ts +35 -0
- package/dist/src/scaffold.d.ts.map +1 -0
- package/dist/src/scaffold.js +166 -0
- package/dist/src/scaffold.js.map +1 -0
- package/dist/src/scaffold.test.d.ts +2 -0
- package/dist/src/scaffold.test.d.ts.map +1 -0
- package/dist/src/scaffold.test.js +204 -0
- package/dist/src/scaffold.test.js.map +1 -0
- package/dist/src/types.d.ts +23 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +28 -0
- package/recipes/11ty-blog/recipe.config.ts +11 -0
- package/recipes/11ty-blog/template/app.ts +18 -0
- package/recipes/11ty-blog/template/content/_data/metadata.js +10 -0
- package/recipes/11ty-blog/template/content/blog/blog.11tydata.json +1 -0
- package/recipes/11ty-blog/template/content/blog/getting-started/index.md +66 -0
- package/recipes/11ty-blog/template/content/blog/hello-world.md +39 -0
- package/recipes/11ty-blog/template/litro.recipe.json +7 -0
- package/recipes/11ty-blog/template/nitro.config.ts +60 -0
- package/recipes/11ty-blog/template/package.json +26 -0
- package/recipes/11ty-blog/template/pages/blog/[slug].ts +65 -0
- package/recipes/11ty-blog/template/pages/blog/index.ts +43 -0
- package/recipes/11ty-blog/template/pages/index.ts +58 -0
- package/recipes/11ty-blog/template/pages/tags/[tag].ts +53 -0
- package/recipes/11ty-blog/template/public/.gitkeep +0 -0
- package/recipes/11ty-blog/template/server/api/hello.ts +6 -0
- package/recipes/11ty-blog/template/server/api/posts.ts +8 -0
- package/recipes/11ty-blog/template/server/middleware/vite-dev.ts +29 -0
- package/recipes/11ty-blog/template/server/routes/[...].ts +55 -0
- package/recipes/11ty-blog/template/tsconfig.json +14 -0
- package/recipes/11ty-blog/template/vite.config.ts +19 -0
- package/recipes/fullstack/recipe.config.ts +10 -0
- package/recipes/fullstack/template/app.ts +20 -0
- package/recipes/fullstack/template/nitro.config.ts +67 -0
- package/recipes/fullstack/template/package.json +26 -0
- package/recipes/fullstack/template/pages/blog/[slug].ts +43 -0
- package/recipes/fullstack/template/pages/blog/index.ts +22 -0
- package/recipes/fullstack/template/pages/index.ts +43 -0
- package/recipes/fullstack/template/public/.gitkeep +0 -0
- package/recipes/fullstack/template/server/api/hello.ts +6 -0
- package/recipes/fullstack/template/server/middleware/vite-dev.ts +31 -0
- package/recipes/fullstack/template/server/routes/[...].ts +59 -0
- package/recipes/fullstack/template/tsconfig.json +14 -0
- package/recipes/fullstack/template/vite.config.ts +15 -0
- package/src/index.ts +229 -0
- package/src/scaffold.test.ts +228 -0
- package/src/scaffold.ts +202 -0
- package/src/types.ts +26 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
import { customElement, state } from 'lit/decorators.js';
|
|
3
|
+
import { LitroPage } from '@beatzball/litro/runtime';
|
|
4
|
+
import { definePageData } from '@beatzball/litro';
|
|
5
|
+
import type { Post } from 'litro:content';
|
|
6
|
+
import { getPosts } from 'litro:content';
|
|
7
|
+
|
|
8
|
+
export interface BlogIndexData {
|
|
9
|
+
posts: Post[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function toDate(d: Date | string): Date { return d instanceof Date ? d : new Date(d as string); }
|
|
13
|
+
|
|
14
|
+
export const pageData = definePageData(async (_event) => {
|
|
15
|
+
const posts = await getPosts();
|
|
16
|
+
return { posts } satisfies BlogIndexData;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
@customElement('page-blog')
|
|
20
|
+
export class BlogIndexPage extends LitroPage {
|
|
21
|
+
@state() declare serverData: BlogIndexData | null;
|
|
22
|
+
|
|
23
|
+
render() {
|
|
24
|
+
const { posts = [] } = this.serverData ?? {};
|
|
25
|
+
return html`
|
|
26
|
+
<main>
|
|
27
|
+
<h1>Blog</h1>
|
|
28
|
+
<ul>
|
|
29
|
+
${posts.map(post => html`
|
|
30
|
+
<li>
|
|
31
|
+
<a href="${post.url}">${post.title}</a>
|
|
32
|
+
<time datetime="${toDate(post.date).toISOString()}">${toDate(post.date).toLocaleDateString()}</time>
|
|
33
|
+
${post.description ? html`<p>${post.description}</p>` : ''}
|
|
34
|
+
</li>
|
|
35
|
+
`)}
|
|
36
|
+
</ul>
|
|
37
|
+
<a href="/">← Home</a>
|
|
38
|
+
</main>
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default BlogIndexPage;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
import { customElement, state } from 'lit/decorators.js';
|
|
3
|
+
import { LitroPage } from '@beatzball/litro/runtime';
|
|
4
|
+
import { definePageData } from '@beatzball/litro';
|
|
5
|
+
import type { Post } from 'litro:content';
|
|
6
|
+
import { getPosts, getGlobalData } from 'litro:content';
|
|
7
|
+
|
|
8
|
+
export interface HomeData {
|
|
9
|
+
recentPosts: Post[];
|
|
10
|
+
siteTitle: string;
|
|
11
|
+
siteDescription: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function toDate(d: Date | string): Date { return d instanceof Date ? d : new Date(d as string); }
|
|
15
|
+
|
|
16
|
+
export const pageData = definePageData(async (_event) => {
|
|
17
|
+
const [recentPosts, metadata] = await Promise.all([
|
|
18
|
+
getPosts({ limit: 5 }),
|
|
19
|
+
getGlobalData(),
|
|
20
|
+
]);
|
|
21
|
+
return {
|
|
22
|
+
recentPosts,
|
|
23
|
+
siteTitle: String(metadata.title ?? '{{projectName}}'),
|
|
24
|
+
siteDescription: String(metadata.description ?? ''),
|
|
25
|
+
} satisfies HomeData;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
@customElement('page-home')
|
|
29
|
+
export class HomePage extends LitroPage {
|
|
30
|
+
@state() declare serverData: HomeData | null;
|
|
31
|
+
|
|
32
|
+
render() {
|
|
33
|
+
const { recentPosts = [], siteTitle = '{{projectName}}', siteDescription = '' } = this.serverData ?? {};
|
|
34
|
+
return html`
|
|
35
|
+
<main>
|
|
36
|
+
<header>
|
|
37
|
+
<h1>${siteTitle}</h1>
|
|
38
|
+
${siteDescription ? html`<p>${siteDescription}</p>` : ''}
|
|
39
|
+
</header>
|
|
40
|
+
<section>
|
|
41
|
+
<h2>Recent Posts</h2>
|
|
42
|
+
<ul>
|
|
43
|
+
${recentPosts.map(post => html`
|
|
44
|
+
<li>
|
|
45
|
+
<a href="${post.url}">${post.title}</a>
|
|
46
|
+
<time datetime="${toDate(post.date).toISOString()}">${toDate(post.date).toLocaleDateString()}</time>
|
|
47
|
+
${post.description ? html`<p>${post.description}</p>` : ''}
|
|
48
|
+
</li>
|
|
49
|
+
`)}
|
|
50
|
+
</ul>
|
|
51
|
+
<p><a href="/blog">All Posts →</a></p>
|
|
52
|
+
</section>
|
|
53
|
+
</main>
|
|
54
|
+
`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default HomePage;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
import { customElement, state } from 'lit/decorators.js';
|
|
3
|
+
import { LitroPage } from '@beatzball/litro/runtime';
|
|
4
|
+
import { definePageData } from '@beatzball/litro';
|
|
5
|
+
import type { Post } from 'litro:content';
|
|
6
|
+
import { getPosts, getTags } from 'litro:content';
|
|
7
|
+
|
|
8
|
+
export interface TagData {
|
|
9
|
+
tag: string;
|
|
10
|
+
posts: Post[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function toDate(d: Date | string): Date { return d instanceof Date ? d : new Date(d as string); }
|
|
14
|
+
|
|
15
|
+
export const pageData = definePageData(async (event) => {
|
|
16
|
+
const tag = event.context.params?.tag ?? '';
|
|
17
|
+
const posts = await getPosts({ tag });
|
|
18
|
+
return { tag, posts } satisfies TagData;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export async function generateRoutes(): Promise<string[]> {
|
|
22
|
+
const tags = await getTags();
|
|
23
|
+
return tags.map(tag => `/tags/${tag}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@customElement('page-tags-tag')
|
|
27
|
+
export class TagPage extends LitroPage {
|
|
28
|
+
@state() declare serverData: TagData | null;
|
|
29
|
+
|
|
30
|
+
render() {
|
|
31
|
+
const { tag = '', posts = [] } = this.serverData ?? {};
|
|
32
|
+
return html`
|
|
33
|
+
<main>
|
|
34
|
+
<h1>Posts tagged: #${tag}</h1>
|
|
35
|
+
<ul>
|
|
36
|
+
${posts.map(post => html`
|
|
37
|
+
<li>
|
|
38
|
+
<a href="${post.url}">${post.title}</a>
|
|
39
|
+
<time datetime="${toDate(post.date).toISOString()}">${toDate(post.date).toLocaleDateString()}</time>
|
|
40
|
+
</li>
|
|
41
|
+
`)}
|
|
42
|
+
</ul>
|
|
43
|
+
<p>
|
|
44
|
+
<a href="/blog">← All Posts</a>
|
|
45
|
+
|
|
|
46
|
+
<a href="/">← Home</a>
|
|
47
|
+
</p>
|
|
48
|
+
</main>
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default TagPage;
|
|
File without changes
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { defineEventHandler, getQuery } from 'h3';
|
|
2
|
+
import { getPosts } from 'litro:content';
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
const query = getQuery(event);
|
|
6
|
+
const tag = typeof query.tag === 'string' ? query.tag : undefined;
|
|
7
|
+
return getPosts({ tag });
|
|
8
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { defineEventHandler, fromNodeMiddleware } from 'h3';
|
|
2
|
+
|
|
3
|
+
let viteHandlerPromise: Promise<ReturnType<typeof fromNodeMiddleware>> | null = null;
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
if (!process.dev || !process.env.NITRO_DEV_WORKER_ID) return;
|
|
7
|
+
|
|
8
|
+
if (!viteHandlerPromise) {
|
|
9
|
+
const httpServer = (event.node.req.socket as import('node:net').Socket & {
|
|
10
|
+
server?: import('node:http').Server;
|
|
11
|
+
}).server;
|
|
12
|
+
|
|
13
|
+
viteHandlerPromise = import('vite')
|
|
14
|
+
.then(({ createServer }) =>
|
|
15
|
+
createServer({
|
|
16
|
+
server: {
|
|
17
|
+
middlewareMode: true,
|
|
18
|
+
hmr: httpServer ? { server: httpServer } : true,
|
|
19
|
+
},
|
|
20
|
+
appType: 'custom',
|
|
21
|
+
root: process.cwd(),
|
|
22
|
+
}),
|
|
23
|
+
)
|
|
24
|
+
.then((server) => fromNodeMiddleware(server.middlewares));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const viteHandler = await viteHandlerPromise;
|
|
28
|
+
return viteHandler(event);
|
|
29
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { defineEventHandler, setResponseHeader, getRequestURL } from 'h3';
|
|
2
|
+
import { createPageHandler } from '@beatzball/litro/runtime/create-page-handler.js';
|
|
3
|
+
import type { LitroRoute } from '@beatzball/litro';
|
|
4
|
+
import { routes, pageModules } from '#litro/page-manifest';
|
|
5
|
+
|
|
6
|
+
function matchRoute(
|
|
7
|
+
pathname: string,
|
|
8
|
+
): { route: LitroRoute; params: Record<string, string> } | undefined {
|
|
9
|
+
for (const route of routes) {
|
|
10
|
+
if (route.isCatchAll) return { route, params: {} };
|
|
11
|
+
|
|
12
|
+
if (!route.isDynamic) {
|
|
13
|
+
if (pathname === route.path) return { route, params: {} };
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const regexStr =
|
|
18
|
+
'^' +
|
|
19
|
+
route.path
|
|
20
|
+
.replace(/:([^/]+)\(\.\*\)\*/g, '(?<$1>.+)')
|
|
21
|
+
.replace(/:([^/?]+)\?/g, '(?<$1>[^/]*)?')
|
|
22
|
+
.replace(/:([^/]+)/g, '(?<$1>[^/]+)') +
|
|
23
|
+
'$';
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const match = pathname.match(new RegExp(regexStr));
|
|
27
|
+
if (match) return { route, params: (match.groups ?? {}) as Record<string, string> };
|
|
28
|
+
} catch {
|
|
29
|
+
// malformed pattern — skip
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default defineEventHandler(async (event) => {
|
|
36
|
+
const pathname = getRequestURL(event).pathname;
|
|
37
|
+
const result = matchRoute(pathname);
|
|
38
|
+
|
|
39
|
+
if (!result) {
|
|
40
|
+
setResponseHeader(event, 'content-type', 'text/html; charset=utf-8');
|
|
41
|
+
return `<!DOCTYPE html>
|
|
42
|
+
<html lang="en"><head><meta charset="UTF-8" /><title>404</title></head>
|
|
43
|
+
<body><h1>404 — Not Found</h1><p>No page matched <code>${pathname}</code>.</p></body>
|
|
44
|
+
</html>`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { route: matched, params } = result;
|
|
48
|
+
event.context.params = { ...event.context.params, ...params };
|
|
49
|
+
|
|
50
|
+
const handler = createPageHandler({
|
|
51
|
+
route: matched,
|
|
52
|
+
pageModule: pageModules[matched.filePath],
|
|
53
|
+
});
|
|
54
|
+
return handler(event);
|
|
55
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"experimentalDecorators": true,
|
|
10
|
+
"useDefineForClassFields": false
|
|
11
|
+
},
|
|
12
|
+
"include": ["**/*.ts", "**/*.tsx"],
|
|
13
|
+
"exclude": ["node_modules", "dist", ".nitro"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import litroContentPlugin from '@beatzball/litro/vite';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [litroContentPlugin()],
|
|
6
|
+
base: '/_litro/',
|
|
7
|
+
resolve: {
|
|
8
|
+
conditions: ['source', 'browser', 'module', 'import', 'default'],
|
|
9
|
+
},
|
|
10
|
+
build: {
|
|
11
|
+
outDir: 'dist/client',
|
|
12
|
+
rollupOptions: {
|
|
13
|
+
input: 'app.ts',
|
|
14
|
+
output: {
|
|
15
|
+
entryFileNames: '[name].js',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recipe.config.d.ts","sourceRoot":"","sources":["../../../recipes/fullstack/recipe.config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,QAAA,MAAM,MAAM,EAAE,WAKb,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recipe.config.js","sourceRoot":"","sources":["../../../recipes/fullstack/recipe.config.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,GAAgB;IAC1B,IAAI,EAAE,WAAW;IACjB,WAAW,EAAE,eAAe;IAC5B,WAAW,EAAE,4DAA4D;IACzE,IAAI,EAAE,MAAM;CACb,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LitroRecipe } from '../../src/types.js';
|
|
2
|
+
|
|
3
|
+
const recipe: LitroRecipe = {
|
|
4
|
+
name: 'fullstack',
|
|
5
|
+
displayName: 'Fullstack App',
|
|
6
|
+
description: 'Full-stack Lit + Nitro app with SSR and blog example pages',
|
|
7
|
+
mode: 'both',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default recipe;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// CRITICAL: must be first — patches LitElement before any component is loaded.
|
|
2
|
+
import '@lit-labs/ssr-client/lit-element-hydrate-support.js';
|
|
3
|
+
|
|
4
|
+
// Client runtime: router outlet and link custom elements.
|
|
5
|
+
import 'litro/runtime/LitroOutlet.js';
|
|
6
|
+
import 'litro/runtime/LitroLink.js';
|
|
7
|
+
|
|
8
|
+
// Routes generated by the page scanner before each vite build.
|
|
9
|
+
// routes.generated.ts lives at the project root (not in dist/) so Vite's
|
|
10
|
+
// emptyOutDir does not delete it between builds.
|
|
11
|
+
import { routes } from './routes.generated.js';
|
|
12
|
+
|
|
13
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
14
|
+
const outlet = document.querySelector('litro-outlet') as (Element & { routes: unknown }) | null;
|
|
15
|
+
if (outlet) {
|
|
16
|
+
outlet.routes = routes;
|
|
17
|
+
} else {
|
|
18
|
+
console.warn('[litro] <litro-outlet> not found — router will not start.');
|
|
19
|
+
}
|
|
20
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { defineNitroConfig } from 'nitropack/config';
|
|
2
|
+
import type { Nitro } from 'nitropack';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { ssrPreset, ssgPreset } from '@beatzball/litro/config';
|
|
5
|
+
import pagesPlugin from '@beatzball/litro/plugins';
|
|
6
|
+
import ssgPlugin from '@beatzball/litro/plugins/ssg';
|
|
7
|
+
|
|
8
|
+
// LITRO_MODE controls the deployment target at build time:
|
|
9
|
+
// LITRO_MODE=server litro build (default — Node.js server)
|
|
10
|
+
// LITRO_MODE=static litro generate (SSG — static HTML for CDN)
|
|
11
|
+
const mode = process.env.LITRO_MODE ?? 'server';
|
|
12
|
+
|
|
13
|
+
export default defineNitroConfig({
|
|
14
|
+
...(mode === 'static' ? ssgPreset() : ssrPreset()),
|
|
15
|
+
|
|
16
|
+
// Nitro auto-discovers server/routes/, server/api/, server/middleware/
|
|
17
|
+
srcDir: 'server',
|
|
18
|
+
|
|
19
|
+
// publicAssets.dir is resolved relative to srcDir ('server/').
|
|
20
|
+
// Use '../dist/client' (not 'dist/client') to reach <rootDir>/dist/client.
|
|
21
|
+
publicAssets: [
|
|
22
|
+
{ dir: '../dist/client', baseURL: '/_litro/', maxAge: 31536000 },
|
|
23
|
+
{ dir: '../public', baseURL: '/', maxAge: 0 },
|
|
24
|
+
],
|
|
25
|
+
|
|
26
|
+
// Must be bundled for edge runtimes (Cloudflare Workers, Vercel Edge).
|
|
27
|
+
externals: { inline: ['@lit-labs/ssr', '@lit-labs/ssr-client'] },
|
|
28
|
+
|
|
29
|
+
// Lit uses legacy experimental decorators.
|
|
30
|
+
esbuild: {
|
|
31
|
+
options: {
|
|
32
|
+
tsconfigRaw: {
|
|
33
|
+
compilerOptions: {
|
|
34
|
+
experimentalDecorators: true,
|
|
35
|
+
useDefineForClassFields: false,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// Exclude vite-dev.ts from auto-discovery so import('vite') never enters
|
|
42
|
+
// the production module graph. Re-register with env:'dev' so it only runs
|
|
43
|
+
// during development.
|
|
44
|
+
ignore: ['**/middleware/vite-dev.ts'],
|
|
45
|
+
handlers: [
|
|
46
|
+
{
|
|
47
|
+
middleware: true,
|
|
48
|
+
handler: resolve('./server/middleware/vite-dev.ts'),
|
|
49
|
+
env: 'dev',
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
|
|
53
|
+
hooks: {
|
|
54
|
+
'build:before': async (nitro: Nitro) => {
|
|
55
|
+
await pagesPlugin(nitro);
|
|
56
|
+
await ssgPlugin(nitro);
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
compatibilityDate: '2025-01-01',
|
|
61
|
+
|
|
62
|
+
routeRules: {
|
|
63
|
+
'/_litro/**': {
|
|
64
|
+
headers: { 'cache-control': 'public, max-age=31536000, immutable' },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"imports": {
|
|
6
|
+
"#litro/page-manifest": "./server/stubs/page-manifest.ts"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "litro dev",
|
|
10
|
+
"build": "litro build",
|
|
11
|
+
"preview": "litro preview",
|
|
12
|
+
"generate": "litro generate"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@beatzball/litro": "latest",
|
|
16
|
+
"lit": "^3.2.1",
|
|
17
|
+
"@lit-labs/ssr": "^3.3.0",
|
|
18
|
+
"@lit-labs/ssr-client": "^1.1.7",
|
|
19
|
+
"h3": "^1.13.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"nitropack": "^2.10.4",
|
|
23
|
+
"vite": "^5.4.11",
|
|
24
|
+
"typescript": "^5.7.3"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { LitElement, html } from 'lit';
|
|
2
|
+
import { customElement, state } from 'lit/decorators.js';
|
|
3
|
+
import { definePageData } from '@beatzball/litro';
|
|
4
|
+
|
|
5
|
+
export interface PostData {
|
|
6
|
+
slug: string;
|
|
7
|
+
title: string;
|
|
8
|
+
content: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Runs on the server; event.context.params contains the matched route params.
|
|
12
|
+
export const pageData = definePageData(async (event) => {
|
|
13
|
+
const slug = event.context.params?.slug ?? '';
|
|
14
|
+
return {
|
|
15
|
+
slug,
|
|
16
|
+
title: `Post: ${slug}`,
|
|
17
|
+
content: `This is the content for the "${slug}" post.`,
|
|
18
|
+
} satisfies PostData;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Tells the SSG which concrete paths to prerender when LITRO_MODE=static.
|
|
22
|
+
export async function generateRoutes(): Promise<string[]> {
|
|
23
|
+
return ['/blog/hello-world', '/blog/getting-started', '/blog/about-litro'];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@customElement('page-blog-slug')
|
|
27
|
+
export class BlogPostPage extends LitElement {
|
|
28
|
+
@state() declare serverData: PostData | null;
|
|
29
|
+
|
|
30
|
+
render() {
|
|
31
|
+
return html`
|
|
32
|
+
<article>
|
|
33
|
+
<h1>${this.serverData?.title ?? 'Loading…'}</h1>
|
|
34
|
+
<p>${this.serverData?.content ?? ''}</p>
|
|
35
|
+
<litro-link href="/blog">← Back to Blog</litro-link>
|
|
36
|
+
|
|
|
37
|
+
<litro-link href="/">← Home</litro-link>
|
|
38
|
+
</article>
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default BlogPostPage;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { LitElement, html } from 'lit';
|
|
2
|
+
import { customElement } from 'lit/decorators.js';
|
|
3
|
+
|
|
4
|
+
@customElement('page-blog')
|
|
5
|
+
export class BlogPage extends LitElement {
|
|
6
|
+
render() {
|
|
7
|
+
return html`
|
|
8
|
+
<main>
|
|
9
|
+
<h1>Blog</h1>
|
|
10
|
+
<p>Choose a post:</p>
|
|
11
|
+
<ul>
|
|
12
|
+
<li><litro-link href="/blog/hello-world">Hello World</litro-link></li>
|
|
13
|
+
<li><litro-link href="/blog/getting-started">Getting Started</litro-link></li>
|
|
14
|
+
<li><litro-link href="/blog/about-litro">About Litro</litro-link></li>
|
|
15
|
+
</ul>
|
|
16
|
+
<litro-link href="/">← Back Home</litro-link>
|
|
17
|
+
</main>
|
|
18
|
+
`;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default BlogPage;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
import { customElement, state } from 'lit/decorators.js';
|
|
3
|
+
import { LitroPage } from '@beatzball/litro/runtime';
|
|
4
|
+
import { definePageData } from '@beatzball/litro';
|
|
5
|
+
|
|
6
|
+
export interface HomeData {
|
|
7
|
+
message: string;
|
|
8
|
+
timestamp: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Runs on the server before SSR — result injected as JSON into the HTML shell.
|
|
12
|
+
export const pageData = definePageData(async (_event) => {
|
|
13
|
+
return {
|
|
14
|
+
message: 'Hello from {{projectName}}!',
|
|
15
|
+
timestamp: new Date().toISOString(),
|
|
16
|
+
} satisfies HomeData;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
@customElement('page-home')
|
|
20
|
+
export class HomePage extends LitroPage {
|
|
21
|
+
@state() declare serverData: HomeData | null;
|
|
22
|
+
|
|
23
|
+
// Called on client-side navigation (not on the initial SSR load).
|
|
24
|
+
override async fetchData() {
|
|
25
|
+
const res = await fetch('/api/hello');
|
|
26
|
+
return res.json() as Promise<HomeData>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
render() {
|
|
30
|
+
if (this.loading) return html`<p>Loading…</p>`;
|
|
31
|
+
return html`
|
|
32
|
+
<main>
|
|
33
|
+
<h1>${this.serverData?.message ?? 'Welcome to {{projectName}}'}</h1>
|
|
34
|
+
<p><small>Rendered at: ${this.serverData?.timestamp ?? '—'}</small></p>
|
|
35
|
+
<nav>
|
|
36
|
+
<litro-link href="/blog">Go to Blog →</litro-link>
|
|
37
|
+
</nav>
|
|
38
|
+
</main>
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default HomePage;
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineEventHandler, fromNodeMiddleware } from 'h3';
|
|
2
|
+
|
|
3
|
+
let viteHandlerPromise: Promise<ReturnType<typeof fromNodeMiddleware>> | null = null;
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
if (!process.dev || !process.env.NITRO_DEV_WORKER_ID) return;
|
|
7
|
+
|
|
8
|
+
if (!viteHandlerPromise) {
|
|
9
|
+
const httpServer = (event.node.req.socket as import('node:net').Socket & {
|
|
10
|
+
server?: import('node:http').Server;
|
|
11
|
+
}).server;
|
|
12
|
+
|
|
13
|
+
viteHandlerPromise = import('vite')
|
|
14
|
+
.then(({ createServer }) =>
|
|
15
|
+
createServer({
|
|
16
|
+
server: {
|
|
17
|
+
middlewareMode: true,
|
|
18
|
+
// Attach Vite HMR WebSocket to Nitro's existing HTTP server so
|
|
19
|
+
// both share a single port with no conflicts.
|
|
20
|
+
hmr: httpServer ? { server: httpServer } : true,
|
|
21
|
+
},
|
|
22
|
+
appType: 'custom',
|
|
23
|
+
root: process.cwd(),
|
|
24
|
+
}),
|
|
25
|
+
)
|
|
26
|
+
.then((server) => fromNodeMiddleware(server.middlewares));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const viteHandler = await viteHandlerPromise;
|
|
30
|
+
return viteHandler(event);
|
|
31
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { defineEventHandler, setResponseHeader, getRequestURL } from 'h3';
|
|
2
|
+
import { createPageHandler } from '@beatzball/litro/runtime/create-page-handler.js';
|
|
3
|
+
import type { LitroRoute } from '@beatzball/litro';
|
|
4
|
+
import { routes, pageModules } from '#litro/page-manifest';
|
|
5
|
+
|
|
6
|
+
function matchRoute(
|
|
7
|
+
pathname: string,
|
|
8
|
+
): { route: LitroRoute; params: Record<string, string> } | undefined {
|
|
9
|
+
for (const route of routes) {
|
|
10
|
+
if (route.isCatchAll) return { route, params: {} };
|
|
11
|
+
|
|
12
|
+
if (!route.isDynamic) {
|
|
13
|
+
if (pathname === route.path) return { route, params: {} };
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Use named capture groups so param values are automatically mapped to names.
|
|
18
|
+
const regexStr =
|
|
19
|
+
'^' +
|
|
20
|
+
route.path
|
|
21
|
+
.replace(/:([^/]+)\(\.\*\)\*/g, '(?<$1>.+)')
|
|
22
|
+
.replace(/:([^/?]+)\?/g, '(?<$1>[^/]*)?')
|
|
23
|
+
.replace(/:([^/]+)/g, '(?<$1>[^/]+)') +
|
|
24
|
+
'$';
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const match = pathname.match(new RegExp(regexStr));
|
|
28
|
+
if (match) return { route, params: (match.groups ?? {}) as Record<string, string> };
|
|
29
|
+
} catch {
|
|
30
|
+
// malformed pattern — skip
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default defineEventHandler(async (event) => {
|
|
37
|
+
const pathname = getRequestURL(event).pathname;
|
|
38
|
+
const result = matchRoute(pathname);
|
|
39
|
+
|
|
40
|
+
if (!result) {
|
|
41
|
+
setResponseHeader(event, 'content-type', 'text/html; charset=utf-8');
|
|
42
|
+
return `<!DOCTYPE html>
|
|
43
|
+
<html lang="en"><head><meta charset="UTF-8" /><title>404</title></head>
|
|
44
|
+
<body><h1>404 — Not Found</h1><p>No page matched <code>${pathname}</code>.</p></body>
|
|
45
|
+
</html>`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { route: matched, params } = result;
|
|
49
|
+
|
|
50
|
+
// Populate route params (e.g. slug from /blog/:slug) on the event context
|
|
51
|
+
// so pageData fetchers can access them via event.context.params.
|
|
52
|
+
event.context.params = { ...event.context.params, ...params };
|
|
53
|
+
|
|
54
|
+
const handler = createPageHandler({
|
|
55
|
+
route: matched,
|
|
56
|
+
pageModule: pageModules[matched.filePath],
|
|
57
|
+
});
|
|
58
|
+
return handler(event);
|
|
59
|
+
});
|