dalila 1.4.4 → 1.5.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/README.md +4 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +346 -0
- package/dist/cli/routes-generator.d.ts +7 -0
- package/dist/cli/routes-generator.js +1020 -0
- package/dist/core/html.d.ts +21 -0
- package/dist/core/html.js +129 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/scope.js +6 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/router/index.d.ts +1 -0
- package/dist/router/index.js +1 -0
- package/dist/router/route-tables.d.ts +128 -0
- package/dist/router/route-tables.js +270 -0
- package/dist/router/router.d.ts +92 -12
- package/dist/router/router.js +1359 -97
- package/dist/runtime/fromHtml.d.ts +35 -0
- package/dist/runtime/fromHtml.js +50 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +1 -0
- package/package.json +12 -3
- package/scripts/dev-server.cjs +122 -12
package/README.md
CHANGED
|
@@ -61,6 +61,10 @@ bind(document.getElementById('app')!, ctx);
|
|
|
61
61
|
- [Template Binding](./docs/runtime/bind.md) — `bind()`, text interpolation, events
|
|
62
62
|
- [FOUC Prevention](./docs/runtime/fouc-prevention.md) — Automatic token hiding
|
|
63
63
|
|
|
64
|
+
### Routing
|
|
65
|
+
|
|
66
|
+
- [Router](./docs/router.md) — Client-side routing with nested layouts, preloading, and file-based route generation
|
|
67
|
+
|
|
64
68
|
### Rendering
|
|
65
69
|
|
|
66
70
|
- [when](./docs/core/when.md) — Conditional visibility
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import { collectHtmlPathDependencyDirs, generateRoutesFile } from './routes-generator.js';
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
const command = args[0];
|
|
7
|
+
const subcommand = args[1];
|
|
8
|
+
const routeArgs = args.slice(2);
|
|
9
|
+
const WATCH_DEBOUNCE_MS = 120;
|
|
10
|
+
function showHelp() {
|
|
11
|
+
console.log(`
|
|
12
|
+
Dalila CLI
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
dalila routes generate [options] Generate routes + manifest from app file structure
|
|
16
|
+
dalila routes init Initialize app and generate routes outputs
|
|
17
|
+
dalila routes watch [options] Watch routes and regenerate outputs on changes
|
|
18
|
+
dalila routes --help Show routes command help
|
|
19
|
+
dalila help Show this help message
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--output <path> Output file (default: ./routes.generated.ts)
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
dalila routes generate
|
|
26
|
+
dalila routes generate --output src/routes.generated.ts
|
|
27
|
+
dalila routes init
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
30
|
+
function showRoutesHelp() {
|
|
31
|
+
console.log(`
|
|
32
|
+
Dalila CLI - Routes
|
|
33
|
+
|
|
34
|
+
Usage:
|
|
35
|
+
dalila routes generate [options] Generate routes + manifest from app file structure
|
|
36
|
+
dalila routes init Initialize app and generate routes outputs
|
|
37
|
+
dalila routes watch [options] Watch routes and regenerate outputs on changes
|
|
38
|
+
dalila routes --help Show this help message
|
|
39
|
+
|
|
40
|
+
Options:
|
|
41
|
+
--output <path> Output file (default: ./routes.generated.ts)
|
|
42
|
+
|
|
43
|
+
Examples:
|
|
44
|
+
dalila routes generate
|
|
45
|
+
dalila routes generate --output src/routes.generated.ts
|
|
46
|
+
dalila routes watch
|
|
47
|
+
dalila routes init
|
|
48
|
+
`);
|
|
49
|
+
}
|
|
50
|
+
function hasHelpFlag(list) {
|
|
51
|
+
return list.includes('--help') || list.includes('-h') || list.includes('help');
|
|
52
|
+
}
|
|
53
|
+
function findProjectRoot(startDir) {
|
|
54
|
+
let current = path.resolve(startDir);
|
|
55
|
+
while (true) {
|
|
56
|
+
if (fs.existsSync(path.join(current, 'package.json'))) {
|
|
57
|
+
return current;
|
|
58
|
+
}
|
|
59
|
+
const parent = path.dirname(current);
|
|
60
|
+
if (parent === current) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
current = parent;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function initRoutes() {
|
|
67
|
+
const appDir = resolveDefaultAppDir(process.cwd());
|
|
68
|
+
if (fs.existsSync(appDir)) {
|
|
69
|
+
console.log('⚠️ App directory already exists');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
console.log('📁 Creating app directory...');
|
|
73
|
+
fs.mkdirSync(appDir, { recursive: true });
|
|
74
|
+
const starterFiles = {
|
|
75
|
+
'layout.html': `<div class="app">
|
|
76
|
+
<header>
|
|
77
|
+
<h1>My App</h1>
|
|
78
|
+
<nav>
|
|
79
|
+
<a d-link="/">Home</a>
|
|
80
|
+
</nav>
|
|
81
|
+
</header>
|
|
82
|
+
<main data-slot="children"></main>
|
|
83
|
+
</div>
|
|
84
|
+
`,
|
|
85
|
+
'page.html': `<div>
|
|
86
|
+
<h2>Home</h2>
|
|
87
|
+
<p>Welcome to your Dalila app!</p>
|
|
88
|
+
</div>
|
|
89
|
+
`
|
|
90
|
+
};
|
|
91
|
+
for (const [filename, content] of Object.entries(starterFiles)) {
|
|
92
|
+
fs.writeFileSync(path.join(appDir, filename), content);
|
|
93
|
+
}
|
|
94
|
+
const appDirLabel = path.relative(process.cwd(), appDir) || appDir;
|
|
95
|
+
const appDirPosix = appDirLabel.replace(/\\/g, '/');
|
|
96
|
+
console.log(`✅ Created app directory with starter files (${appDirLabel}):`);
|
|
97
|
+
for (const filename of Object.keys(starterFiles)) {
|
|
98
|
+
console.log(` ${path.join(appDirLabel, filename).replace(/\\/g, '/')}`);
|
|
99
|
+
}
|
|
100
|
+
const outputPath = path.join(process.cwd(), 'routes.generated.ts');
|
|
101
|
+
console.log('🧩 Generating routes outputs...');
|
|
102
|
+
try {
|
|
103
|
+
await generateRoutesFile(appDir, outputPath);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
console.error('❌ Error generating routes:', error);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
console.log('');
|
|
110
|
+
console.log('Next steps:');
|
|
111
|
+
console.log(' 1. Customize your app routes');
|
|
112
|
+
console.log(` 2. Add segments with page.html (e.g. ${appDirPosix}/about/page.html)`);
|
|
113
|
+
console.log(` 3. Add dynamic slugs with folders like ${appDirPosix}/blog/[slug]/page.html`);
|
|
114
|
+
console.log(' 4. Optional: add page.ts/layout.ts/middleware.ts for logic/guards');
|
|
115
|
+
console.log(' 5. Run: dalila routes generate (after changing app files)');
|
|
116
|
+
console.log('');
|
|
117
|
+
}
|
|
118
|
+
function resolveDefaultAppDir(cwd) {
|
|
119
|
+
const resolvedCwd = path.resolve(cwd);
|
|
120
|
+
const projectRoot = findProjectRoot(resolvedCwd);
|
|
121
|
+
if (!projectRoot) {
|
|
122
|
+
return path.join(resolvedCwd, 'src', 'app');
|
|
123
|
+
}
|
|
124
|
+
const appRoot = path.join(projectRoot, 'src', 'app');
|
|
125
|
+
if (resolvedCwd === appRoot || resolvedCwd.startsWith(appRoot + path.sep)) {
|
|
126
|
+
return resolvedCwd;
|
|
127
|
+
}
|
|
128
|
+
const relToRoot = path.relative(projectRoot, resolvedCwd);
|
|
129
|
+
if (!relToRoot || relToRoot.startsWith('..')) {
|
|
130
|
+
return appRoot;
|
|
131
|
+
}
|
|
132
|
+
if (relToRoot === 'src' || relToRoot.startsWith('src' + path.sep)) {
|
|
133
|
+
return appRoot;
|
|
134
|
+
}
|
|
135
|
+
return path.join(appRoot, relToRoot);
|
|
136
|
+
}
|
|
137
|
+
function resolveGenerateConfig(cliArgs, cwd = process.cwd()) {
|
|
138
|
+
const dirIndex = cliArgs.indexOf('--dir');
|
|
139
|
+
const outputIndex = cliArgs.indexOf('--output');
|
|
140
|
+
if (dirIndex !== -1) {
|
|
141
|
+
console.error('❌ --dir is no longer supported. Dalila now resolves app dir automatically from src/app.');
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
if (outputIndex !== -1 && !cliArgs[outputIndex + 1]) {
|
|
145
|
+
console.error('❌ Missing value for --output');
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
const appDir = dirIndex !== -1
|
|
149
|
+
? path.resolve(cwd, cliArgs[dirIndex + 1])
|
|
150
|
+
: resolveDefaultAppDir(cwd);
|
|
151
|
+
const outputPath = outputIndex !== -1
|
|
152
|
+
? path.resolve(cwd, cliArgs[outputIndex + 1])
|
|
153
|
+
: path.join(cwd, 'routes.generated.ts');
|
|
154
|
+
return { appDir, outputPath };
|
|
155
|
+
}
|
|
156
|
+
async function generateRoutes(cliArgs) {
|
|
157
|
+
const { appDir, outputPath } = resolveGenerateConfig(cliArgs);
|
|
158
|
+
console.log('');
|
|
159
|
+
console.log('🚀 Dalila Routes Generator');
|
|
160
|
+
console.log('');
|
|
161
|
+
try {
|
|
162
|
+
await generateRoutesFile(appDir, outputPath);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
console.error('❌ Error generating routes:', error);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function collectRouteDirs(rootDir) {
|
|
170
|
+
const dirs = [];
|
|
171
|
+
function visit(dir) {
|
|
172
|
+
dirs.push(dir);
|
|
173
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
174
|
+
for (const entry of entries) {
|
|
175
|
+
if (!entry.isDirectory())
|
|
176
|
+
continue;
|
|
177
|
+
if (entry.name.startsWith('.'))
|
|
178
|
+
continue;
|
|
179
|
+
visit(path.join(dir, entry.name));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
visit(rootDir);
|
|
183
|
+
return dirs;
|
|
184
|
+
}
|
|
185
|
+
function resolveExistingWatchDir(targetDir) {
|
|
186
|
+
let current = path.resolve(targetDir);
|
|
187
|
+
while (true) {
|
|
188
|
+
if (fs.existsSync(current)) {
|
|
189
|
+
try {
|
|
190
|
+
if (fs.statSync(current).isDirectory()) {
|
|
191
|
+
return current;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// Ignore FS races while files are being created/deleted.
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const parent = path.dirname(current);
|
|
199
|
+
if (parent === current)
|
|
200
|
+
return null;
|
|
201
|
+
current = parent;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function watchRoutes(cliArgs) {
|
|
205
|
+
const { appDir, outputPath } = resolveGenerateConfig(cliArgs);
|
|
206
|
+
const watchedDirs = new Map();
|
|
207
|
+
const outputAbsPath = path.resolve(outputPath);
|
|
208
|
+
const outputBasePath = outputAbsPath.endsWith('.ts') ? outputAbsPath.slice(0, -3) : outputAbsPath;
|
|
209
|
+
const generatedOutputPaths = new Set([
|
|
210
|
+
outputAbsPath,
|
|
211
|
+
`${outputBasePath}.manifest.ts`,
|
|
212
|
+
`${outputBasePath}.types.ts`
|
|
213
|
+
]);
|
|
214
|
+
let regenerateTimer = null;
|
|
215
|
+
if (!fs.existsSync(appDir)) {
|
|
216
|
+
console.error('❌ App directory not found:', appDir);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
console.log('');
|
|
220
|
+
console.log('👀 Dalila Routes Watch');
|
|
221
|
+
console.log(` app: ${appDir}`);
|
|
222
|
+
console.log(` output: ${outputPath}`);
|
|
223
|
+
console.log('');
|
|
224
|
+
const runGenerate = async () => {
|
|
225
|
+
try {
|
|
226
|
+
await generateRoutesFile(appDir, outputPath);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
console.error('❌ Error generating routes:', error);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
const refreshWatchers = () => {
|
|
233
|
+
const nextDirs = new Set(collectRouteDirs(appDir).map(d => path.resolve(d)));
|
|
234
|
+
for (const dependencyDir of collectHtmlPathDependencyDirs(appDir)) {
|
|
235
|
+
const resolvedWatchDir = resolveExistingWatchDir(dependencyDir);
|
|
236
|
+
if (resolvedWatchDir) {
|
|
237
|
+
nextDirs.add(resolvedWatchDir);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
for (const [dir, watcher] of watchedDirs.entries()) {
|
|
241
|
+
if (!nextDirs.has(dir)) {
|
|
242
|
+
watcher.close();
|
|
243
|
+
watchedDirs.delete(dir);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
for (const dir of nextDirs) {
|
|
247
|
+
if (watchedDirs.has(dir))
|
|
248
|
+
continue;
|
|
249
|
+
try {
|
|
250
|
+
const watcher = fs.watch(dir, (_eventType, filename) => {
|
|
251
|
+
if (filename) {
|
|
252
|
+
const changedAbsPath = path.resolve(dir, filename.toString());
|
|
253
|
+
if (generatedOutputPaths.has(changedAbsPath)) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (regenerateTimer)
|
|
258
|
+
clearTimeout(regenerateTimer);
|
|
259
|
+
regenerateTimer = setTimeout(() => {
|
|
260
|
+
refreshWatchers();
|
|
261
|
+
console.log('♻️ Route change detected, regenerating...');
|
|
262
|
+
runGenerate();
|
|
263
|
+
}, WATCH_DEBOUNCE_MS);
|
|
264
|
+
});
|
|
265
|
+
watcher.on('error', err => {
|
|
266
|
+
console.error(`❌ Watch error in ${dir}:`, err);
|
|
267
|
+
});
|
|
268
|
+
watchedDirs.set(dir, watcher);
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
console.error(`❌ Failed to watch directory ${dir}:`, error);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
const stop = () => {
|
|
276
|
+
if (regenerateTimer)
|
|
277
|
+
clearTimeout(regenerateTimer);
|
|
278
|
+
for (const watcher of watchedDirs.values()) {
|
|
279
|
+
watcher.close();
|
|
280
|
+
}
|
|
281
|
+
watchedDirs.clear();
|
|
282
|
+
console.log('\n🛑 Stopped routes watch');
|
|
283
|
+
};
|
|
284
|
+
runGenerate();
|
|
285
|
+
refreshWatchers();
|
|
286
|
+
process.on('SIGINT', () => {
|
|
287
|
+
stop();
|
|
288
|
+
process.exit(0);
|
|
289
|
+
});
|
|
290
|
+
process.on('SIGTERM', () => {
|
|
291
|
+
stop();
|
|
292
|
+
process.exit(0);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
// Main
|
|
296
|
+
async function main() {
|
|
297
|
+
if (command === 'help' || !command) {
|
|
298
|
+
showHelp();
|
|
299
|
+
}
|
|
300
|
+
else if (command === 'routes') {
|
|
301
|
+
if (!subcommand || subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
302
|
+
showRoutesHelp();
|
|
303
|
+
}
|
|
304
|
+
else if (subcommand === 'generate') {
|
|
305
|
+
if (hasHelpFlag(routeArgs)) {
|
|
306
|
+
showRoutesHelp();
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
await generateRoutes(routeArgs);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
else if (subcommand === 'watch') {
|
|
313
|
+
if (hasHelpFlag(routeArgs)) {
|
|
314
|
+
showRoutesHelp();
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
watchRoutes(routeArgs);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
else if (subcommand === 'init') {
|
|
321
|
+
if (hasHelpFlag(routeArgs)) {
|
|
322
|
+
showRoutesHelp();
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
await initRoutes();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
console.error('Unknown subcommand:', subcommand);
|
|
330
|
+
showRoutesHelp();
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
else if (command === '--help' || command === '-h') {
|
|
335
|
+
showHelp();
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
console.error('Unknown command:', command);
|
|
339
|
+
showHelp();
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
main().catch((error) => {
|
|
344
|
+
console.error('❌ Unexpected error:', error);
|
|
345
|
+
process.exit(1);
|
|
346
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function collectHtmlPathDependencyDirs(routesDir: string): string[];
|
|
2
|
+
/**
|
|
3
|
+
* Generate route files from the app directory.
|
|
4
|
+
*
|
|
5
|
+
* Produces three outputs: route table, route manifest, and route types.
|
|
6
|
+
*/
|
|
7
|
+
export declare function generateRoutesFile(routesDir: string, outputPath: string): Promise<void>;
|