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 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,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -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>;