create-l5e 0.1.0-alpha.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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +44 -0
  3. package/bin/create-l5e.js +275 -0
  4. package/package.json +25 -0
  5. package/templates/basic/_gitignore +4 -0
  6. package/templates/basic/index.html +12 -0
  7. package/templates/basic/package.json +30 -0
  8. package/templates/basic/public/robots.txt +2 -0
  9. package/templates/basic/server.ts +8 -0
  10. package/templates/basic/src/client.global.ts +1 -0
  11. package/templates/basic/src/entry-server.ts +4 -0
  12. package/templates/basic/src/global.css +48 -0
  13. package/templates/basic/src/middleware.ts +17 -0
  14. package/templates/basic/src/route.ts +7 -0
  15. package/templates/basic/src/views/actions/actions.css +7 -0
  16. package/templates/basic/src/views/actions/actions.tsx +8 -0
  17. package/templates/basic/src/views/actions/client.ts +10 -0
  18. package/templates/basic/src/views/actions/index.tsx +32 -0
  19. package/templates/basic/src/views/home/home.css +13 -0
  20. package/templates/basic/src/views/home/index.tsx +34 -0
  21. package/templates/basic/src/views/home/loader.ts +17 -0
  22. package/templates/basic/src/vite-env.d.ts +5 -0
  23. package/templates/basic/tsconfig.json +25 -0
  24. package/templates/basic/vite.config.js +36 -0
  25. package/templates/minimal/_gitignore +4 -0
  26. package/templates/minimal/index.html +12 -0
  27. package/templates/minimal/package.json +30 -0
  28. package/templates/minimal/public/robots.txt +2 -0
  29. package/templates/minimal/server.ts +8 -0
  30. package/templates/minimal/src/client.global.ts +1 -0
  31. package/templates/minimal/src/entry-server.ts +4 -0
  32. package/templates/minimal/src/global.css +41 -0
  33. package/templates/minimal/src/route.ts +6 -0
  34. package/templates/minimal/src/views/home/home.css +7 -0
  35. package/templates/minimal/src/views/home/index.tsx +29 -0
  36. package/templates/minimal/src/views/home/loader.ts +17 -0
  37. package/templates/minimal/src/vite-env.d.ts +5 -0
  38. package/templates/minimal/tsconfig.json +25 -0
  39. package/templates/minimal/vite.config.js +36 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 L5E contributors
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.
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # create-l5e
2
+
3
+ Create a L5E app from the official starter template.
4
+
5
+ ```sh
6
+ npm create l5e@alpha my-app -- --template basic
7
+ ```
8
+
9
+ Equivalent commands:
10
+
11
+ ```sh
12
+ npx create-l5e@alpha my-app --template basic
13
+ pnpm create l5e@alpha my-app --template basic
14
+ bun create l5e@alpha my-app --template basic
15
+ ```
16
+
17
+ Available templates:
18
+
19
+ - `basic`: example app with middleware rewrite, loader cache headers, action and swap interaction
20
+ - `minimal`: small app with one server-rendered page
21
+
22
+ Run the dev server immediately after install:
23
+
24
+ ```sh
25
+ pnpm create l5e@alpha my-app --template basic --dev
26
+ ```
27
+
28
+ ## Publishing
29
+
30
+ The package name must be `create-l5e`. npm maps `npm create l5e` to the
31
+ `create-l5e` package name.
32
+
33
+ Publish the alpha:
34
+
35
+ ```sh
36
+ pnpm --filter create-l5e publish --tag alpha
37
+ ```
38
+
39
+ After the package is stable, publish or move the dist tag to `latest` so users can run:
40
+
41
+ ```sh
42
+ npm create l5e@latest my-app
43
+ npx create-l5e@latest my-app
44
+ ```
@@ -0,0 +1,275 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+
10
+ const helpText = `
11
+ Usage:
12
+ create-l5e <project-directory> [options]
13
+
14
+ Options:
15
+ -t, --template <name> Use a first-party template
16
+ --dev Start the dev server after installing
17
+ --no-install Copy the starter without installing dependencies
18
+ -p, --package-manager <name> Use npm, pnpm, yarn, or bun
19
+ -h, --help Show this help
20
+
21
+ Templates:
22
+ basic Example app with middleware, loaders, actions, and swap
23
+ minimal Small app with one server-rendered page
24
+
25
+ Examples:
26
+ pnpm create l5e@alpha my-app --template basic
27
+ pnpm create l5e@alpha my-app --template minimal
28
+ `;
29
+
30
+ const builtInTemplates = new Map([
31
+ ['basic', '../templates/basic'],
32
+ ['minimal', '../templates/minimal'],
33
+ ]);
34
+ const packageManagers = new Set(['npm', 'pnpm', 'yarn', 'bun']);
35
+
36
+ main().catch((error) => {
37
+ console.error(error instanceof Error ? error.message : String(error));
38
+ process.exit(1);
39
+ });
40
+
41
+ async function main() {
42
+ const options = parseArgs(process.argv.slice(2));
43
+
44
+ if (options.help) {
45
+ console.log(helpText.trim());
46
+ return;
47
+ }
48
+
49
+ if (!options.targetDir) {
50
+ console.error(helpText.trim());
51
+ process.exit(1);
52
+ }
53
+
54
+ if (options.dev && !options.install) {
55
+ throw new Error('The --dev option requires dependency installation. Remove --no-install.');
56
+ }
57
+
58
+ const targetDir = path.resolve(process.cwd(), options.targetDir);
59
+ const projectName = toPackageName(path.basename(targetDir));
60
+ const template = resolveTemplate(options.template);
61
+ const packageManager = options.packageManager ?? detectPackageManager();
62
+
63
+ await ensureEmptyDirectory(targetDir);
64
+ await writeTemplate(template, targetDir, { projectName });
65
+ await updatePackageName(targetDir, projectName);
66
+
67
+ console.log(`Created ${projectName} from ${template.name} in ${targetDir}`);
68
+
69
+ if (options.install) {
70
+ runCommand(packageManager, ['install'], targetDir);
71
+ }
72
+
73
+ if (options.dev) {
74
+ runCommand(packageManager, devArgs(packageManager), targetDir);
75
+ return;
76
+ }
77
+
78
+ printNextSteps(targetDir, packageManager, options.install);
79
+ }
80
+
81
+ function parseArgs(args) {
82
+ const options = {
83
+ dev: false,
84
+ help: false,
85
+ install: true,
86
+ packageManager: undefined,
87
+ template: 'basic',
88
+ targetDir: undefined,
89
+ };
90
+
91
+ for (let index = 0; index < args.length; index += 1) {
92
+ const arg = args[index];
93
+
94
+ if (arg === '--help' || arg === '-h') {
95
+ options.help = true;
96
+ continue;
97
+ }
98
+
99
+ if (arg === '--dev') {
100
+ options.dev = true;
101
+ continue;
102
+ }
103
+
104
+ if (arg === '--no-install') {
105
+ options.install = false;
106
+ continue;
107
+ }
108
+
109
+ if (arg === '--template' || arg === '-t') {
110
+ const value = args[index + 1];
111
+ if (!value) throw new Error(`${arg} requires a first-party template name.`);
112
+ options.template = value;
113
+ index += 1;
114
+ continue;
115
+ }
116
+
117
+ if (arg.startsWith('--template=')) {
118
+ options.template = arg.slice('--template='.length);
119
+ continue;
120
+ }
121
+
122
+ if (arg === '--package-manager' || arg === '-p') {
123
+ const value = args[index + 1];
124
+ if (!value) throw new Error(`${arg} requires npm, pnpm, yarn, or bun.`);
125
+ options.packageManager = normalizePackageManager(value);
126
+ index += 1;
127
+ continue;
128
+ }
129
+
130
+ if (arg.startsWith('--package-manager=')) {
131
+ options.packageManager = normalizePackageManager(arg.slice('--package-manager='.length));
132
+ continue;
133
+ }
134
+
135
+ if (arg.startsWith('-')) {
136
+ throw new Error(`Unknown option: ${arg}`);
137
+ }
138
+
139
+ if (options.targetDir) {
140
+ throw new Error(`Unexpected argument: ${arg}`);
141
+ }
142
+
143
+ options.targetDir = arg;
144
+ }
145
+
146
+ return options;
147
+ }
148
+
149
+ function normalizePackageManager(value) {
150
+ const normalized = value.trim().toLowerCase();
151
+ if (!packageManagers.has(normalized)) {
152
+ throw new Error(`Unsupported package manager: ${value}. Use npm, pnpm, yarn, or bun.`);
153
+ }
154
+ return normalized;
155
+ }
156
+
157
+ function detectPackageManager() {
158
+ const userAgent = process.env.npm_config_user_agent ?? '';
159
+
160
+ if (userAgent.startsWith('pnpm')) return 'pnpm';
161
+ if (userAgent.startsWith('yarn')) return 'yarn';
162
+ if (userAgent.startsWith('bun')) return 'bun';
163
+ return 'npm';
164
+ }
165
+
166
+ function toPackageName(name) {
167
+ const normalized = name
168
+ .trim()
169
+ .toLowerCase()
170
+ .replace(/^[._]/, '')
171
+ .replace(/[^a-z0-9._-]+/g, '-')
172
+ .replace(/-+/g, '-')
173
+ .replace(/^-|-$/g, '');
174
+
175
+ return normalized || 'l5e-app';
176
+ }
177
+
178
+ function resolveTemplate(value) {
179
+ const templateName = value.trim();
180
+ const builtInPath = builtInTemplates.get(templateName.toLowerCase());
181
+
182
+ if (builtInPath) {
183
+ return {
184
+ type: 'local',
185
+ name: templateName,
186
+ dir: path.resolve(__dirname, builtInPath),
187
+ };
188
+ }
189
+
190
+ const builtIns = [...builtInTemplates.keys()].join(', ');
191
+ throw new Error(`Unknown template: ${value}. Use one of: ${builtIns}.`);
192
+ }
193
+
194
+ async function ensureEmptyDirectory(dir) {
195
+ await fs.mkdir(dir, { recursive: true });
196
+
197
+ const entries = await fs.readdir(dir);
198
+ if (entries.length > 0) {
199
+ throw new Error(`Target directory is not empty: ${dir}`);
200
+ }
201
+ }
202
+
203
+ async function writeTemplate(template, targetDir, replacements) {
204
+ await copyTemplate(template.dir, targetDir, replacements);
205
+ }
206
+
207
+ async function copyTemplate(sourceDir, targetDir, replacements) {
208
+ const entries = await fs.readdir(sourceDir, { withFileTypes: true });
209
+
210
+ for (const entry of entries) {
211
+ const sourcePath = path.join(sourceDir, entry.name);
212
+ const targetName = entry.name === '_gitignore' ? '.gitignore' : entry.name;
213
+ const targetPath = path.join(targetDir, targetName);
214
+
215
+ if (entry.isDirectory()) {
216
+ await fs.mkdir(targetPath, { recursive: true });
217
+ await copyTemplate(sourcePath, targetPath, replacements);
218
+ continue;
219
+ }
220
+
221
+ let content = await fs.readFile(sourcePath, 'utf8');
222
+ content = content.replaceAll('__PROJECT_NAME__', replacements.projectName);
223
+ await fs.writeFile(targetPath, content);
224
+ }
225
+ }
226
+
227
+ async function updatePackageName(targetDir, projectName) {
228
+ const packageJsonPath = path.join(targetDir, 'package.json');
229
+ let content;
230
+
231
+ try {
232
+ content = await fs.readFile(packageJsonPath, 'utf8');
233
+ } catch (error) {
234
+ if (error && error.code === 'ENOENT') return;
235
+ throw error;
236
+ }
237
+
238
+ const packageJson = JSON.parse(content);
239
+ packageJson.name = projectName;
240
+ await fs.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
241
+ }
242
+
243
+ function runCommand(command, args, cwd) {
244
+ console.log(`Running ${[command, ...args].join(' ')}...`);
245
+
246
+ const result = spawnSync(command, args, {
247
+ cwd,
248
+ stdio: 'inherit',
249
+ shell: process.platform === 'win32',
250
+ });
251
+
252
+ if (result.status !== 0) {
253
+ const exitCode = typeof result.status === 'number' ? result.status : 1;
254
+ process.exit(exitCode);
255
+ }
256
+ }
257
+
258
+ function devArgs(packageManager) {
259
+ if (packageManager === 'npm') return ['run', 'dev'];
260
+ if (packageManager === 'bun') return ['run', 'dev'];
261
+ return ['dev'];
262
+ }
263
+
264
+ function printNextSteps(targetDir, packageManager, installed) {
265
+ const relativeTarget = path.relative(process.cwd(), targetDir) || '.';
266
+ const devCommand = [packageManager, ...devArgs(packageManager)].join(' ');
267
+
268
+ console.log('');
269
+ console.log('Next steps:');
270
+ console.log(` cd ${relativeTarget}`);
271
+ if (!installed) {
272
+ console.log(` ${packageManager} install`);
273
+ }
274
+ console.log(` ${devCommand}`);
275
+ }
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "create-l5e",
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "Create an L5E app from the official starter template.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "create-l5e": "./bin/create-l5e.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "templates",
13
+ "README.md"
14
+ ],
15
+ "engines": {
16
+ "node": ">=20.19.0"
17
+ },
18
+ "publishConfig": {
19
+ "tag": "alpha"
20
+ },
21
+ "scripts": {
22
+ "build": "node --check bin/create-l5e.js",
23
+ "typecheck": "node --check bin/create-l5e.js"
24
+ }
25
+ }
@@ -0,0 +1,4 @@
1
+ node_modules
2
+ dist
3
+ .env
4
+ .env.local
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <!--app-head-->
7
+ </head>
8
+ <body>
9
+ <!--app-html-->
10
+ <!--app-scripts-->
11
+ </body>
12
+ </html>
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "__PROJECT_NAME__",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "tsx server.ts",
8
+ "build": "vite build && vite build --ssr src/entry-server.ts --outDir dist/server && vite build --ssr server.ts --outDir dist --emptyOutDir false",
9
+ "preview": "cross-env NODE_ENV=production node dist/server.js",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "@withl5e/l5e": "^0.1.0-alpha.0",
14
+ "react": "^19.0.0",
15
+ "react-dom": "^19.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/react": "^19.0.0",
19
+ "@types/react-dom": "^19.0.0",
20
+ "cross-env": "^7.0.3",
21
+ "tsx": "^4.7.0",
22
+ "typescript": "^5.6.3",
23
+ "vite": "^7.1.5"
24
+ },
25
+ "pnpm": {
26
+ "onlyBuiltDependencies": [
27
+ "esbuild"
28
+ ]
29
+ }
30
+ }
@@ -0,0 +1,2 @@
1
+ User-agent: *
2
+ Disallow:
@@ -0,0 +1,8 @@
1
+ import { startServer } from '@withl5e/l5e/server';
2
+
3
+ startServer({
4
+ root: process.cwd(),
5
+ port: Number(process.env.PORT) || 5173,
6
+ base: '/',
7
+ publicDir: './public',
8
+ });
@@ -0,0 +1 @@
1
+ import './global.css';
@@ -0,0 +1,4 @@
1
+ export { loadMiddleware, render } from '@withl5e/l5e/entry-server';
2
+ export { runInRenderContext } from '@withl5e/l5e/jsx-runtime';
3
+ export { renderJsxToHtmlString } from '@withl5e/l5e';
4
+ export { viewActions } from 'virtual:l5e-actions';
@@ -0,0 +1,48 @@
1
+ * {
2
+ box-sizing: border-box;
3
+ }
4
+
5
+ body {
6
+ margin: 0;
7
+ font-family:
8
+ Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
9
+ color: #17202a;
10
+ background: #f6f7f9;
11
+ }
12
+
13
+ a {
14
+ color: #0f6bff;
15
+ }
16
+
17
+ main {
18
+ width: min(920px, calc(100% - 32px));
19
+ margin: 48px auto;
20
+ }
21
+
22
+ .nav {
23
+ display: flex;
24
+ gap: 16px;
25
+ padding: 16px 24px;
26
+ border-bottom: 1px solid #d9dee7;
27
+ background: #fff;
28
+ }
29
+
30
+ .panel {
31
+ padding: 24px;
32
+ border: 1px solid #d9dee7;
33
+ border-radius: 8px;
34
+ background: #fff;
35
+ }
36
+
37
+ .button {
38
+ display: inline-flex;
39
+ align-items: center;
40
+ justify-content: center;
41
+ min-height: 40px;
42
+ padding: 0 14px;
43
+ border: 1px solid #aeb8c8;
44
+ border-radius: 6px;
45
+ color: #17202a;
46
+ background: #fff;
47
+ cursor: pointer;
48
+ }
@@ -0,0 +1,17 @@
1
+ import { defineMiddleware, sequence } from '@withl5e/l5e/middleware';
2
+
3
+ const rewriteDemo = defineMiddleware((context, next) => {
4
+ if (context.url.pathname === '/rewrite-demo') {
5
+ return next('/');
6
+ }
7
+
8
+ return next();
9
+ });
10
+
11
+ const addPoweredBy = defineMiddleware(async (_context, next) => {
12
+ const response = await next();
13
+ response.headers.set('x-powered-by', 'L5E');
14
+ return response;
15
+ });
16
+
17
+ export const onRequest = sequence(rewriteDemo, addPoweredBy);
@@ -0,0 +1,7 @@
1
+ import type { RequestInfo } from '@withl5e/l5e/entry-server';
2
+
3
+ export default function routeHandler(requestInfo: RequestInfo) {
4
+ if (requestInfo.pathname === '/') return 'home';
5
+ if (requestInfo.pathname === '/actions') return 'actions';
6
+ return null;
7
+ }
@@ -0,0 +1,7 @@
1
+ .actions-panel h1 {
2
+ margin: 0 0 12px;
3
+ }
4
+
5
+ .actions-panel button {
6
+ margin: 16px 0;
7
+ }
@@ -0,0 +1,8 @@
1
+ import { defineAction } from '@withl5e/l5e/action';
2
+
3
+ export const loadServerTime = defineAction({
4
+ method: 'GET',
5
+ handler: () => {
6
+ return <span data-swap-target="server-time">{new Date().toISOString()}</span>;
7
+ },
8
+ });
@@ -0,0 +1,10 @@
1
+ import { createSwap } from '@withl5e/l5e/swap';
2
+ import { loadServerTime } from './actions';
3
+
4
+ createSwap({
5
+ trigger: '[data-load-time]',
6
+ target: '[data-swap-target="server-time"]',
7
+ select: '[data-swap-target="server-time"]',
8
+ swap: 'outerHTML',
9
+ action: () => loadServerTime({}),
10
+ });
@@ -0,0 +1,32 @@
1
+ import { Fragment, useClientJs, useCss } from '@withl5e/l5e/jsx-runtime';
2
+ import { MetadataRenderer } from '@withl5e/l5e/seo';
3
+
4
+ export default function ActionsPage() {
5
+ useCss('./actions.css');
6
+ useClientJs('./client.ts');
7
+
8
+ return (
9
+ <>
10
+ <MetadataRenderer metadata={{ title: 'Action + swap example' }} />
11
+ <nav class="nav" aria-label="Primary">
12
+ <a href="/">Home</a>
13
+ <a href="/rewrite-demo">Rewrite demo</a>
14
+ <a href="/actions">Action + swap</a>
15
+ </nav>
16
+ <main>
17
+ <section class="panel actions-panel">
18
+ <h1>Action + swap</h1>
19
+ <p>
20
+ Click the button to call a server action and swap the returned HTML into the page.
21
+ </p>
22
+ <button class="button" type="button" data-load-time>
23
+ Load server time
24
+ </button>
25
+ <p>
26
+ Current value: <span data-swap-target="server-time">not loaded</span>
27
+ </p>
28
+ </section>
29
+ </main>
30
+ </>
31
+ );
32
+ }
@@ -0,0 +1,13 @@
1
+ .home-panel h1 {
2
+ margin: 0 0 12px;
3
+ }
4
+
5
+ .home-panel p {
6
+ max-width: 64ch;
7
+ }
8
+
9
+ .timestamp {
10
+ margin-top: 24px;
11
+ color: #5f6f85;
12
+ font-size: 0.95rem;
13
+ }
@@ -0,0 +1,34 @@
1
+ import { Fragment, useCss } from '@withl5e/l5e/jsx-runtime';
2
+ import { MetadataRenderer } from '@withl5e/l5e/seo';
3
+
4
+ import type { HomeLoaderData } from './loader';
5
+
6
+ export default function HomePage({ now }: HomeLoaderData) {
7
+ useCss('./home.css');
8
+
9
+ return (
10
+ <>
11
+ <MetadataRenderer
12
+ metadata={{
13
+ title: 'L5E basic example',
14
+ description: 'A minimal L5E app with middleware rewrite and cache headers.',
15
+ }}
16
+ />
17
+ <nav class="nav" aria-label="Primary">
18
+ <a href="/">Home</a>
19
+ <a href="/rewrite-demo">Rewrite demo</a>
20
+ <a href="/actions">Action + swap</a>
21
+ </nav>
22
+ <main>
23
+ <section class="panel home-panel">
24
+ <h1>L5E basic example</h1>
25
+ <p>
26
+ This page is server-rendered. Visiting <a href="/rewrite-demo">/rewrite-demo</a>{' '}
27
+ renders the same home page through middleware rewrite.
28
+ </p>
29
+ <p class="timestamp">Rendered at {now}</p>
30
+ </section>
31
+ </main>
32
+ </>
33
+ );
34
+ }
@@ -0,0 +1,17 @@
1
+ import type { LoaderFunction, LoaderResult } from '@withl5e/l5e/entry-server';
2
+
3
+ export type HomeLoaderData = {
4
+ now: string;
5
+ };
6
+
7
+ export const loader: LoaderFunction = async (): Promise<LoaderResult> => {
8
+ return {
9
+ props: {
10
+ now: new Date().toISOString(),
11
+ },
12
+ maxAge: 0,
13
+ sMaxAge: 60,
14
+ swr: 300,
15
+ cacheTags: ['home'],
16
+ };
17
+ };
@@ -0,0 +1,5 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ declare module 'virtual:l5e-actions' {
4
+ export const viewActions: Record<string, () => Promise<Record<string, unknown>>>;
5
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "types": ["vite/client"],
6
+ "moduleResolution": "Bundler",
7
+ "lib": ["ES2024", "DOM"],
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "resolveJsonModule": true,
12
+ "allowJs": false,
13
+ "noEmit": true,
14
+ "sourceMap": true,
15
+ "isolatedModules": true,
16
+ "jsx": "preserve",
17
+ "jsxFactory": "jsxFactory",
18
+ "jsxFragmentFactory": "Fragment",
19
+ "baseUrl": ".",
20
+ "paths": {
21
+ "~/*": ["src/*"]
22
+ }
23
+ },
24
+ "include": ["src/**/*", "server.ts", "vite.config.js"]
25
+ }
@@ -0,0 +1,36 @@
1
+ import { coreVite } from '@withl5e/l5e/vite-plugin';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { defineConfig } from 'vite';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ export default defineConfig({
10
+ esbuild: {
11
+ jsx: 'preserve',
12
+ },
13
+ plugins: [coreVite()],
14
+ build: {
15
+ outDir: 'dist/client',
16
+ manifest: true,
17
+ rollupOptions: {
18
+ external: (id) => id === 'fsevents',
19
+ },
20
+ },
21
+ optimizeDeps: {
22
+ exclude: ['@withl5e/l5e'],
23
+ },
24
+ ssr: {
25
+ noExternal: ['@withl5e/l5e'],
26
+ external: ['rollup', 'esbuild', 'fsevents'],
27
+ },
28
+ resolve: {
29
+ alias: {
30
+ '~': path.resolve(__dirname, 'src'),
31
+ },
32
+ },
33
+ server: {
34
+ port: 5173,
35
+ },
36
+ });
@@ -0,0 +1,4 @@
1
+ node_modules
2
+ dist
3
+ .env
4
+ .env.local
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <!--app-head-->
7
+ </head>
8
+ <body>
9
+ <!--app-html-->
10
+ <!--app-scripts-->
11
+ </body>
12
+ </html>
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "__PROJECT_NAME__",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "tsx server.ts",
8
+ "build": "vite build && vite build --ssr src/entry-server.ts --outDir dist/server && vite build --ssr server.ts --outDir dist --emptyOutDir false",
9
+ "preview": "cross-env NODE_ENV=production node dist/server.js",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "@withl5e/l5e": "^0.1.0-alpha.0",
14
+ "react": "^19.0.0",
15
+ "react-dom": "^19.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/react": "^19.0.0",
19
+ "@types/react-dom": "^19.0.0",
20
+ "cross-env": "^7.0.3",
21
+ "tsx": "^4.7.0",
22
+ "typescript": "^5.6.3",
23
+ "vite": "^7.1.5"
24
+ },
25
+ "pnpm": {
26
+ "onlyBuiltDependencies": [
27
+ "esbuild"
28
+ ]
29
+ }
30
+ }
@@ -0,0 +1,2 @@
1
+ User-agent: *
2
+ Allow: /
@@ -0,0 +1,8 @@
1
+ import { startServer } from '@withl5e/l5e/server';
2
+
3
+ startServer({
4
+ root: process.cwd(),
5
+ port: Number(process.env.PORT) || 5173,
6
+ base: '/',
7
+ publicDir: './public',
8
+ });
@@ -0,0 +1 @@
1
+ import './global.css';
@@ -0,0 +1,4 @@
1
+ export { loadMiddleware, render } from '@withl5e/l5e/entry-server';
2
+ export { runInRenderContext } from '@withl5e/l5e/jsx-runtime';
3
+ export { renderJsxToHtmlString } from '@withl5e/l5e';
4
+ export { viewActions } from 'virtual:l5e-actions';
@@ -0,0 +1,41 @@
1
+ * {
2
+ box-sizing: border-box;
3
+ }
4
+
5
+ body {
6
+ margin: 0;
7
+ font-family:
8
+ Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
9
+ color: #17202a;
10
+ background: #f6f7f9;
11
+ }
12
+
13
+ a {
14
+ color: #0f6bff;
15
+ }
16
+
17
+ main {
18
+ width: min(920px, calc(100% - 32px));
19
+ margin: 48px auto;
20
+ }
21
+
22
+ .nav {
23
+ display: flex;
24
+ gap: 16px;
25
+ padding: 16px 24px;
26
+ border-bottom: 1px solid #d9dee7;
27
+ background: #fff;
28
+ }
29
+
30
+ .panel {
31
+ padding: 24px;
32
+ border: 1px solid #d9dee7;
33
+ border-radius: 8px;
34
+ background: #fff;
35
+ }
36
+
37
+ .timestamp {
38
+ margin-top: 24px;
39
+ color: #5f6f85;
40
+ font-size: 0.95rem;
41
+ }
@@ -0,0 +1,6 @@
1
+ import type { RequestInfo } from '@withl5e/l5e/entry-server';
2
+
3
+ export default function routeHandler(requestInfo: RequestInfo) {
4
+ if (requestInfo.pathname === '/') return 'home';
5
+ return null;
6
+ }
@@ -0,0 +1,7 @@
1
+ .home-panel h1 {
2
+ margin: 0 0 12px;
3
+ }
4
+
5
+ .home-panel p {
6
+ max-width: 64ch;
7
+ }
@@ -0,0 +1,29 @@
1
+ import { Fragment, useCss } from '@withl5e/l5e/jsx-runtime';
2
+ import { MetadataRenderer } from '@withl5e/l5e/seo';
3
+
4
+ import type { HomeLoaderData } from './loader';
5
+
6
+ export default function HomePage({ now }: HomeLoaderData) {
7
+ useCss('./home.css');
8
+
9
+ return (
10
+ <>
11
+ <MetadataRenderer
12
+ metadata={{
13
+ title: 'L5E starter',
14
+ description: 'A minimal L5E app.',
15
+ }}
16
+ />
17
+ <nav class="nav" aria-label="Primary">
18
+ <a href="/">Home</a>
19
+ </nav>
20
+ <main>
21
+ <section class="panel home-panel">
22
+ <h1>L5E starter</h1>
23
+ <p>This page is server-rendered by L5E.</p>
24
+ <p class="timestamp">Rendered at {now}</p>
25
+ </section>
26
+ </main>
27
+ </>
28
+ );
29
+ }
@@ -0,0 +1,17 @@
1
+ import type { LoaderFunction, LoaderResult } from '@withl5e/l5e/entry-server';
2
+
3
+ export type HomeLoaderData = {
4
+ now: string;
5
+ };
6
+
7
+ export const loader: LoaderFunction = async (): Promise<LoaderResult> => {
8
+ return {
9
+ props: {
10
+ now: new Date().toISOString(),
11
+ },
12
+ maxAge: 0,
13
+ sMaxAge: 60,
14
+ swr: 300,
15
+ cacheTags: ['home'],
16
+ };
17
+ };
@@ -0,0 +1,5 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ declare module 'virtual:l5e-actions' {
4
+ export const viewActions: Record<string, () => Promise<Record<string, unknown>>>;
5
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "types": ["vite/client"],
6
+ "moduleResolution": "Bundler",
7
+ "lib": ["ES2024", "DOM"],
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "resolveJsonModule": true,
12
+ "allowJs": false,
13
+ "noEmit": true,
14
+ "sourceMap": true,
15
+ "isolatedModules": true,
16
+ "jsx": "preserve",
17
+ "jsxFactory": "jsxFactory",
18
+ "jsxFragmentFactory": "Fragment",
19
+ "baseUrl": ".",
20
+ "paths": {
21
+ "~/*": ["src/*"]
22
+ }
23
+ },
24
+ "include": ["src/**/*", "server.ts", "vite.config.js"]
25
+ }
@@ -0,0 +1,36 @@
1
+ import { coreVite } from '@withl5e/l5e/vite-plugin';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { defineConfig } from 'vite';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ export default defineConfig({
10
+ esbuild: {
11
+ jsx: 'preserve',
12
+ },
13
+ plugins: [coreVite()],
14
+ build: {
15
+ outDir: 'dist/client',
16
+ manifest: true,
17
+ rollupOptions: {
18
+ external: (id) => id === 'fsevents',
19
+ },
20
+ },
21
+ optimizeDeps: {
22
+ exclude: ['@withl5e/l5e'],
23
+ },
24
+ ssr: {
25
+ noExternal: ['@withl5e/l5e'],
26
+ external: ['rollup', 'esbuild', 'fsevents'],
27
+ },
28
+ resolve: {
29
+ alias: {
30
+ '~': path.resolve(__dirname, 'src'),
31
+ },
32
+ },
33
+ server: {
34
+ port: 5173,
35
+ },
36
+ });