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.
- package/LICENSE +21 -0
- package/README.md +44 -0
- package/bin/create-l5e.js +275 -0
- package/package.json +25 -0
- package/templates/basic/_gitignore +4 -0
- package/templates/basic/index.html +12 -0
- package/templates/basic/package.json +30 -0
- package/templates/basic/public/robots.txt +2 -0
- package/templates/basic/server.ts +8 -0
- package/templates/basic/src/client.global.ts +1 -0
- package/templates/basic/src/entry-server.ts +4 -0
- package/templates/basic/src/global.css +48 -0
- package/templates/basic/src/middleware.ts +17 -0
- package/templates/basic/src/route.ts +7 -0
- package/templates/basic/src/views/actions/actions.css +7 -0
- package/templates/basic/src/views/actions/actions.tsx +8 -0
- package/templates/basic/src/views/actions/client.ts +10 -0
- package/templates/basic/src/views/actions/index.tsx +32 -0
- package/templates/basic/src/views/home/home.css +13 -0
- package/templates/basic/src/views/home/index.tsx +34 -0
- package/templates/basic/src/views/home/loader.ts +17 -0
- package/templates/basic/src/vite-env.d.ts +5 -0
- package/templates/basic/tsconfig.json +25 -0
- package/templates/basic/vite.config.js +36 -0
- package/templates/minimal/_gitignore +4 -0
- package/templates/minimal/index.html +12 -0
- package/templates/minimal/package.json +30 -0
- package/templates/minimal/public/robots.txt +2 -0
- package/templates/minimal/server.ts +8 -0
- package/templates/minimal/src/client.global.ts +1 -0
- package/templates/minimal/src/entry-server.ts +4 -0
- package/templates/minimal/src/global.css +41 -0
- package/templates/minimal/src/route.ts +6 -0
- package/templates/minimal/src/views/home/home.css +7 -0
- package/templates/minimal/src/views/home/index.tsx +29 -0
- package/templates/minimal/src/views/home/loader.ts +17 -0
- package/templates/minimal/src/vite-env.d.ts +5 -0
- package/templates/minimal/tsconfig.json +25 -0
- 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,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 @@
|
|
|
1
|
+
import './global.css';
|
|
@@ -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,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,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,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,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 @@
|
|
|
1
|
+
import './global.css';
|
|
@@ -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,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,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
|
+
});
|