@ape-egg/vibe 1.6.0 → 1.7.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 CHANGED
@@ -1,5 +1,66 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.7.0] - 2026-02-19
4
+
5
+ ### Fixed
6
+
7
+ - **Watch mode stale component cache** - Incremental recompilation no longer uses stale component content
8
+ - Component cache is cleared before every recompile trigger, ensuring components are always re-read from disk
9
+ - Component cache and parser element cache are also invalidated immediately when a component file changes
10
+ - Fixes corrupted output (old event handlers, wrong attributes) that appeared on every watch-mode save after the first
11
+
12
+ - **Custom element transform — nested hyphenated tags** (`elementsAsIs: false`)
13
+ - `<accordion-content>` inside `<accordion>` previously produced `<div class="accordion" -content>` due to the `<accordion>` regex partially matching the longer tag name
14
+ - Fixed by requiring whitespace or end-of-tag immediately after the tag name in the opening tag regex
15
+ - Tags are now also sorted by descending length so more specific names (e.g. `accordion-content`) are always processed before their prefixes (`accordion`)
16
+
17
+ - **Custom element transform — existing class preserved** (`elementsAsIs: false`)
18
+ - `<crow class="i-should-preserve">` previously compiled to `<div class="crow">`, discarding the original class
19
+ - Existing `class="..."` is now merged: result is `<div class="crow i-should-preserve">`
20
+
21
+ - **`elementsAsIs` config respected in dependency scanning**
22
+ - `fetch_components_for_files` and `fetch_all_components` were hardcoded to `elements_as_is: false`, ignoring the project config
23
+ - Both functions now correctly read `self.config.elements_as_is` and `self.config.reserved_elements`
24
+
25
+ - **`vibe-dehydrate` protected during manifest stamping**
26
+ - Content inside `<template vibe-dehydrate>` was incorrectly processed during value stamping
27
+ - Dehydrated regions are now extracted before processing and restored afterwards
28
+
29
+ - **Conditional evaluation errors handled gracefully**
30
+ - A failed `<!-- if ... -->` expression (undefined variable, syntax error) now defaults to `false` instead of crashing
31
+
32
+ ### Changed
33
+
34
+ - **Lifecycle event API** — `vibe()` now returns an object that supports `.on(event, callback)` before boot completes
35
+ - `$.on('ready', cb)` — fires once after all components load and initial processing completes
36
+ - `$.on('afterUpdate', cb)` — fires on every state change
37
+ - `$.on('afterDomMutation', cb)` — fires after every DOM mutation batch
38
+ - Listeners registered before boot are queued and replayed once the runtime is ready
39
+
40
+ - **Component `onComplete` timing** — deferred via `queueMicrotask` to give user code a chance to register listeners before the ready callback fires
41
+
42
+ - **Iteration performance** — `cloneTreeNode` avoids spread operator; `nameBindings` now correctly carried through cloned iteration trees
43
+
44
+ ---
45
+
46
+ ## [1.6.1] - 2026-02-12
47
+
48
+ ### Fixed
49
+
50
+ - **Compiler output directory handling** - Fixed duplicate file generation in output
51
+ - Compiler now properly handles source and output paths without preserving source directory structure
52
+ - Files from `./src` are now correctly written to root of output directory (e.g., `compiled/index.html`) instead of `compiled/src/index.html`
53
+ - Canonicalized source path for reliable comparison and path stripping
54
+ - Updated all processing functions to use canonical source path for relative path calculation
55
+
56
+ - **Manifest detection improvements** - Better handling of directory URLs and extensionless paths
57
+ - Directory URLs now normalized: `/compiled/` → `/compiled/index.html`
58
+ - Extensionless URLs now normalized: `/compiled/mypage` → `/compiled/mypage.html`
59
+ - Fixes manifest detection when visiting pages without explicit `.html` extension
60
+ - Enables proper hyperspeed loading for directory index pages
61
+
62
+ ---
63
+
3
64
  ## [1.6.0] - 2026-02-12
4
65
 
5
66
  ### Added
@@ -522,7 +583,7 @@
522
583
  - **Iteration**: `<!-- each items as item, i -->` with efficient diffing
523
584
  - **Nested iteration**: `<!-- each category.items as item -->`
524
585
  - **Conditionals**: `<!-- if condition -->...<!-- else -->...<!-- /if -->`
525
- - **Dehydrate**: `<div dehydrate>` to skip reactive processing
586
+ - **Dehydrate**: `<div vibe-dehydrate>` to skip reactive processing
526
587
  - **Expression evaluation**: `@[count * 2]`, `@[firstName + ' ' + lastName]`
527
588
 
528
589
  ### Performance
package/README.md CHANGED
@@ -1,13 +1,15 @@
1
1
  # Vibe
2
2
 
3
- A runtime-first reactive library.
3
+ **Version 1.6.0 (Alpha)** — A runtime-first reactive framework with optional compilation.
4
4
 
5
- No virtual DOM. No build step. Just modern JavaScript.
5
+ No virtual DOM. No build step required. Just modern JavaScript. When you need production optimizations, add the optional Rust-based compiler.
6
6
 
7
7
  ```bash
8
8
  npm install @ape-egg/vibe
9
9
  ```
10
10
 
11
+ **Status:** Functional and ready to use, but expect bugs and breaking changes daily until stable. Use in production at your own risk.
12
+
11
13
  ---
12
14
 
13
15
  ## Vibe Runtime
@@ -21,9 +23,9 @@ The core reactive runtime. Works directly in the browser without any build tools
21
23
  <head>
22
24
  <link rel="stylesheet" href="./node_modules/@ape-egg/vibe/vibe.css" />
23
25
  <script type="module">
24
- import state from './node_modules/@ape-egg/vibe/runtime/index.js';
25
- // Attaches to element with attribute "vibe" by default
26
- window.$ = state({ name: 'World', count: 0 });
26
+ import vibe from './node_modules/@ape-egg/vibe/index.js';
27
+
28
+ vibe({ name: 'World', count: 0 });
27
29
  </script>
28
30
  </head>
29
31
  <body vibe-fouc>
@@ -121,7 +123,7 @@ Runtime component loading with props and slots:
121
123
  Skip reactive processing for an element:
122
124
 
123
125
  ```html
124
- <code dehydrate>@[this] displays literally</code>
126
+ <code vibe-dehydrate>@[this] displays literally</code>
125
127
  ```
126
128
 
127
129
  ### Reserved Words & Gotchas
@@ -131,28 +133,35 @@ Vibe uses specific patterns and keywords that have special meaning. Avoid using
131
133
  #### Classes & Attributes
132
134
 
133
135
  - **`vibe-fouc`** — Class or attribute for FOUC (Flash of Unstyled Content) prevention. Automatically removed after hydration completes.
136
+
134
137
  ```html
135
- <body vibe-fouc> <!-- or class="vibe-fouc" -->
138
+ <body vibe-fouc>
139
+ <!-- or class="vibe-fouc" -->
140
+ </body>
136
141
  ```
137
142
 
138
143
  - **`vibe-dehydrate`** — Class or attribute to skip reactive processing. Useful for displaying literal `@[...]` syntax in documentation.
139
144
  ```html
140
- <code vibe-dehydrate>@[variable]</code> <!-- or class="vibe-dehydrate" -->
145
+ <code vibe-dehydrate>@[variable]</code>
146
+ <!-- or class="vibe-dehydrate" -->
141
147
  ```
142
148
 
143
149
  #### Element Names & Classes
144
150
 
145
151
  - **`<component>`** — Element name for component system. Used with `src` attribute for runtime component loading, or as a wrapper for inlined components.
152
+
146
153
  ```html
147
154
  <component src="/path/to/component.html"></component>
148
155
  ```
149
156
 
150
157
  - **`class="component"`** — Alternative syntax for components using standard HTML elements. Useful for HTML validation or accessibility.
158
+
151
159
  ```html
152
160
  <div class="component" src="/path/to/component.html"></div>
153
161
  ```
154
162
 
155
163
  - **`<slot>`** — Element name for component content injection. Gets replaced with content passed between component tags.
164
+
156
165
  ```html
157
166
  <!-- In component file -->
158
167
  <slot></slot>
@@ -166,6 +175,7 @@ Vibe uses specific patterns and keywords that have special meaning. Avoid using
166
175
  #### Comment Syntax
167
176
 
168
177
  - **`<!-- each -->`** / **`<!-- /each -->`** — Iteration block markers.
178
+
169
179
  ```html
170
180
  <!-- each items as item -->
171
181
  <!-- /each -->
@@ -188,9 +198,10 @@ Vibe uses specific patterns and keywords that have special meaning. Avoid using
188
198
  #### Global Properties
189
199
 
190
200
  - **`window.$`** — Global reactive state object. All reactive data should be accessed through this.
201
+
191
202
  ```javascript
192
203
  window.$ = state({ count: 0 });
193
- $.count++; // Triggers reactive updates
204
+ $.count++; // Triggers reactive updates
194
205
  ```
195
206
 
196
207
  - **`window.__vibeManifest`** — Internal manifest data. Used by the compiler for optimization. Don't modify.
@@ -206,13 +217,14 @@ Vibe uses specific patterns and keywords that have special meaning. Avoid using
206
217
  - **`vibe:ready`** — Custom event fired when Vibe completes initial hydration.
207
218
  ```javascript
208
219
  document.addEventListener('vibe:ready', () => {
209
- console.log('Vibe is ready');
220
+ console.info('Vibe is ready');
210
221
  });
211
222
  ```
212
223
 
213
224
  #### Special Attribute Meanings
214
225
 
215
226
  - **`src`** on **`<component>`** or **`<div class="component">`** — Triggers runtime component fetching. Components without `src` are treated as inline wrappers.
227
+
216
228
  ```html
217
229
  <!-- Both work the same way -->
218
230
  <component src="/components/card.html"></component>
@@ -246,7 +258,7 @@ No need for immutable update patterns or spread operators. Just mutate and Vibe
246
258
 
247
259
  ## Vibe Compiler
248
260
 
249
- Optional build step for production optimization. The compiler processes HTML files, inlines components, and handles assets while preserving directory structure.
261
+ Optional build step for production optimization. The compiler provides component inlining, iteration optimization, watch mode, and hydration manifests while preserving directory structure.
250
262
 
251
263
  ### Installation
252
264
 
@@ -264,18 +276,27 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
264
276
  # Initialize config in package.json
265
277
  bunx vibe compile --init
266
278
 
267
- # Compile
279
+ # Basic compilation
268
280
  bunx vibe compile
281
+ # or shorthand
282
+ bunx vibe c
283
+
284
+ # Watch mode (incremental compilation)
285
+ bunx vibe compile --watch
286
+
287
+ # Production build
288
+ bunx vibe compile --minify --source-maps
269
289
 
270
290
  # With options
271
291
  bunx vibe compile --verbose # Step-by-step logging
272
292
  bunx vibe compile --minify # Minify output
273
- bunx vibe compile --accessibility # Transform custom tags to divs
293
+ bunx vibe compile --elements-as-is # Keep custom elements as-is
274
294
  bunx vibe compile --validate # Validate HTML syntax
275
- bunx vibe compile --create-manifest # Generate hydration manifest
276
295
  bunx vibe compile --source-maps # Generate source maps
277
296
  bunx vibe compile --node-modules-as-is # Copy node_modules as-is
278
- bunx vibe compile --watch # Watch mode (future)
297
+ bunx vibe compile --components-as-is # Skip component inlining
298
+ bunx vibe compile --runtime-as-is # Skip manifest generation
299
+ bunx vibe compile --iterations-as-is # Skip iteration optimization
279
300
  ```
280
301
 
281
302
  Or via npm scripts:
@@ -283,13 +304,14 @@ Or via npm scripts:
283
304
  ```json
284
305
  {
285
306
  "scripts": {
286
- "vibe:compile": "vibe compile",
287
- "vibe:compile:init": "vibe compile --init"
307
+ "compile": "vibe compile",
308
+ "compile:watch": "vibe compile --watch",
309
+ "compile:prod": "vibe compile --minify --source-maps"
288
310
  }
289
311
  }
290
312
  ```
291
313
 
292
- Then run with `bun vibe:compile` or `npm run vibe:compile`.
314
+ Then run with `npm run compile` or `bun compile`.
293
315
 
294
316
  ### Configuration
295
317
 
@@ -304,13 +326,26 @@ Add to your `package.json`:
304
326
  "pages": "pages",
305
327
  "assets": "assets",
306
328
  "minify": false,
307
- "accessibility": false,
308
- "excludeTags": [],
309
- "nodeModulesAsIs": false
329
+ "elementsAsIs": false,
330
+ "reservedElements": [],
331
+ "sourceMaps": false,
332
+ "validate": false,
333
+ "nodeModulesAsIs": false,
334
+ "componentsAsIs": false,
335
+ "runtimeAsIs": false,
336
+ "iterationsAsIs": false
310
337
  }
311
338
  }
312
339
  ```
313
340
 
341
+ **Key Options:**
342
+
343
+ - `elementsAsIs: false` — Transform custom elements to divs (default)
344
+ - `reservedElements: []` — Additional element names to reserve (appends to built-in HTML5 elements + "component")
345
+ - `componentsAsIs: false` — Inline components (default) or keep separate for runtime
346
+ - `iterationsAsIs: false` — Optimize iterations (default) or use runtime rendering
347
+ - `runtimeAsIs: false` — Generate manifest (default) or skip for runtime-only
348
+
314
349
  **Defaults** (when no config):
315
350
 
316
351
  - `source`: `./`
@@ -351,6 +386,67 @@ Auto-detects based on lockfiles: `bun.lockb`, `pnpm-lock.yaml`, `yarn.lock`, `pa
351
386
  **Opt-out:**
352
387
  Set `nodeModulesAsIs: true` in config or use `--node-modules-as-is` flag to copy node_modules as-is.
353
388
 
389
+ ### Watch Mode
390
+
391
+ Watch mode enables incremental compilation with intelligent dependency tracking:
392
+
393
+ ```bash
394
+ bunx vibe compile --watch
395
+ ```
396
+
397
+ Features:
398
+
399
+ - **Incremental builds** — Only recompiles changed files (~100ms)
400
+ - **Dependency tracking** — Changes to components trigger recompilation of pages using them
401
+ - **Debounced** — 300ms debounce prevents excessive compilation during rapid changes
402
+ - **Delta output** — First compile shows full output, subsequent compiles show only changes
403
+
404
+ ### Component System
405
+
406
+ Components are automatically inlined during compilation with full support for props and slots:
407
+
408
+ ```html
409
+ <!-- Source: components/card.html -->
410
+ <div class="card">
411
+ <h2>@[title]</h2>
412
+ <p>@[description]</p>
413
+ <slot></slot>
414
+ </div>
415
+
416
+ <!-- Usage in page -->
417
+ <component src="/components/card.html" title="My Card" description="Card description">
418
+ <p>Slot content</p>
419
+ </component>
420
+
421
+ <!-- Compiled output -->
422
+ <component>
423
+ <div class="card">
424
+ <h2>My Card</h2>
425
+ <p>Card description</p>
426
+ <p>Slot content</p>
427
+ </div>
428
+ </component>
429
+ ```
430
+
431
+ **Custom element syntax:** Components can also use custom element syntax (e.g., `<card>` → auto-converts to `<component src="/components/card.html">`).
432
+
433
+ **Opt-out:** Set `componentsAsIs: true` to keep components as separate files for runtime loading.
434
+
435
+ ### Iteration Optimization
436
+
437
+ The compiler generates optimized batch functions for `<!-- each -->` loops, providing 2-3x faster rendering:
438
+
439
+ ```html
440
+ <!-- Source -->
441
+ <!-- each items as item, index -->
442
+ <li>@[index]: @[item]</li>
443
+ <!-- /each -->
444
+
445
+ <!-- Compiled to optimized batch function -->
446
+ ```
447
+
448
+ **Opt-out:** Set `iterationsAsIs: true` to use runtime rendering for all iterations.
449
+
354
450
  ### Output Structure
355
451
 
356
452
  Mirrors source structure:
@@ -368,9 +464,9 @@ src/ compiled/
368
464
  └── vibe/ └── vibe/
369
465
  ```
370
466
 
371
- ### Accessibility Mode
467
+ ### Element Transformation
372
468
 
373
- `--accessibility` transforms custom HTML elements to divs with classes:
469
+ By default (`elementsAsIs: false`), custom HTML elements are transformed to divs with classes for better HTML validity:
374
470
 
375
471
  ```html
376
472
  <!-- input -->
@@ -380,7 +476,23 @@ src/ compiled/
380
476
  <div class="counter-header">Count</div>
381
477
  ```
382
478
 
383
- Exclude specific tags (e.g., web components) via `excludeTags` config.
479
+ Set `elementsAsIs: true` or use `--elements-as-is` flag to keep custom elements unchanged.
480
+
481
+ **Note:** The `<component>` element is a framework element and is never transformed.
482
+
483
+ ### Reserved Element Names
484
+
485
+ The compiler validates component filenames against reserved element names (all HTML5 elements + "component" + your custom list). This prevents confusing bugs where components share names with HTML elements.
486
+
487
+ ```json
488
+ {
489
+ "vibe-compiler": {
490
+ "reservedElements": ["my-custom-element", "another-reserved-name"]
491
+ }
492
+ }
493
+ ```
494
+
495
+ **Case-sensitive validation:** `nav.html` conflicts with `<nav>` and will error, but `Nav.html` is allowed.
384
496
 
385
497
  ### Building Native Binaries
386
498
 
package/boot.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // Used by both index.js (global state) and component.js (component state)
3
3
 
4
4
  import main from './runtime/index.js';
5
+ import { getPendingListeners } from './index.js';
5
6
 
6
7
  let bootQueued = false;
7
8
  let booted = false;
@@ -39,6 +40,16 @@ export const boot = () => {
39
40
  // Boot with merged state
40
41
  window.$ = main(mergedState, config, targetSelector);
41
42
 
43
+ // Apply pending listeners from vibe instance
44
+ const pendingListeners = getPendingListeners();
45
+ if (pendingListeners) {
46
+ Object.keys(pendingListeners).forEach(event => {
47
+ pendingListeners[event].forEach(callback => {
48
+ window.$.on(event, callback);
49
+ });
50
+ });
51
+ }
52
+
42
53
  return window.$;
43
54
  };
44
55
 
@@ -2061,7 +2061,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
2061
2061
 
2062
2062
  [[package]]
2063
2063
  name = "vibe-compiler"
2064
- version = "0.1.0"
2064
+ version = "1.7.0"
2065
2065
  dependencies = [
2066
2066
  "clap",
2067
2067
  "colored",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "vibe-compiler"
3
- version = "0.1.0"
3
+ version = "1.7.0"
4
4
  edition = "2021"
5
5
  description = "Vibe framework compiler - compiles Vibe source files into optimized output"
6
6
  authors = ["Kim Korte"]
@@ -40,9 +40,8 @@ const MIRROR_EXTENSIONS: &[&str] = &[
40
40
 
41
41
  // Files and directories to skip when walking source (supports glob patterns)
42
42
  // Note: Output directory is checked dynamically (not hardcoded here)
43
+ // Note: Dotfiles are handled by starts_with('.') check in should_skip_path()
43
44
  pub const SKIP_FILES: &[&str] = &[
44
- ".git",
45
- ".claude",
46
45
  "target", // Rust build artifacts
47
46
  "tests", // Test files
48
47
  "test-results",
@@ -56,6 +55,11 @@ pub const SKIP_FILES: &[&str] = &[
56
55
 
57
56
  /// Check if a path should be skipped based on SKIP_FILES patterns
58
57
  pub fn should_skip_path(path: &Path, name: &str) -> bool {
58
+ // Check if name starts with dot (dotfiles/directories)
59
+ if name.starts_with('.') {
60
+ return true;
61
+ }
62
+
59
63
  // Check exact name match (for directories and simple filenames)
60
64
  if SKIP_FILES.contains(&name) {
61
65
  return true;
@@ -433,6 +437,11 @@ impl Compiler {
433
437
  }
434
438
  }
435
439
 
440
+ /// Clear all cached component content (forces re-fetch on next compile)
441
+ pub fn clear_component_cache(&mut self) {
442
+ self.component_cache.clear();
443
+ }
444
+
436
445
  pub fn compile(&mut self) -> Result<CompileStats, CompileError> {
437
446
  let mut stats = CompileStats {
438
447
  files_compiled: 0,
@@ -492,6 +501,19 @@ impl Compiler {
492
501
  }
493
502
  }
494
503
 
504
+ // Clean output directory if not in no-clean mode
505
+ if !self.config.no_clean && self.config.output.exists() {
506
+ if self.verbose {
507
+ println!(" Cleaning output directory: {}", self.config.output.display());
508
+ }
509
+ fs::remove_dir_all(&self.config.output).map_err(|e| {
510
+ CompileError::WriteError {
511
+ path: self.config.output.display().to_string(),
512
+ source: e,
513
+ }
514
+ })?;
515
+ }
516
+
495
517
  // Create output directory
496
518
  if !self.config.output.exists() {
497
519
  fs::create_dir_all(&self.config.output).map_err(|_| {
@@ -502,18 +524,20 @@ impl Compiler {
502
524
  }
503
525
  }
504
526
 
505
- // Canonicalize output path once for reliable comparison
527
+ // Canonicalize both output and source paths for reliable comparison
506
528
  let canonical_output = self.config.output.canonicalize()
507
529
  .unwrap_or_else(|_| self.config.output.clone());
530
+ let canonical_source = self.config.source.canonicalize()
531
+ .unwrap_or_else(|_| self.config.source.clone());
508
532
 
509
533
  // Track compile and copy times separately
510
534
  let compile_start = Instant::now();
511
535
 
512
536
  // Process all HTML files in source (excluding components directory)
513
- self.process_directory_html_only(&self.config.source.clone(), &parser, "", &canonical_output, &mut stats)?;
537
+ self.process_directory_html_only(&canonical_source, &parser, "", &canonical_output, &canonical_source, &mut stats)?;
514
538
 
515
539
  // Process source directory for assets (CSS, JS, images, etc.)
516
- self.process_directory_assets_only(&self.config.source.clone(), "", &canonical_output, &mut stats)?;
540
+ self.process_directory_assets_only(&canonical_source, "", &canonical_output, &canonical_source, &mut stats)?;
517
541
 
518
542
  let process_time = compile_start.elapsed();
519
543
 
@@ -798,10 +822,9 @@ impl Compiler {
798
822
  // Stamp values (pre-render) AFTER building manifest
799
823
  // When components_as_is is true, skip stamping inside component elements (runtime will handle them)
800
824
  let stamper = ValueStamper::new(&state, components_as_is)?;
801
- let mut pre_rendered = stamper.stamp_html(html.to_string())?;
825
+ let pre_rendered = stamper.stamp_html(html.to_string())?;
802
826
 
803
- // Remove FOUC prevention since HTML is pre-rendered
804
- pre_rendered = remove_fouc_prevention(pre_rendered);
827
+ // Note: FOUC removal happens in compile_html_file(), not here
805
828
 
806
829
  // Write pre-rendered HTML
807
830
  fs::write(html_path, pre_rendered)
@@ -906,6 +929,7 @@ impl Compiler {
906
929
  parser: &HtmlParser,
907
930
  relative_path: &str,
908
931
  canonical_output: &Path,
932
+ canonical_source: &Path,
909
933
  stats: &mut CompileStats,
910
934
  ) -> Result<(), CompileError> {
911
935
  let entries = fs::read_dir(dir).map_err(|e| CompileError::ReadError {
@@ -941,7 +965,7 @@ impl Compiler {
941
965
  format!("{}/{}", relative_path, file_name)
942
966
  };
943
967
 
944
- self.process_directory_html_only(&path, parser, &new_relative, canonical_output, stats)?;
968
+ self.process_directory_html_only(&path, parser, &new_relative, canonical_output, canonical_source, stats)?;
945
969
  } else if path.extension().and_then(|e| e.to_str()) == Some("html") {
946
970
  let (internal, external, component_srcs) = self.compile_html_file(&path, parser, relative_path)?;
947
971
  stats.files_compiled += 1;
@@ -962,7 +986,7 @@ impl Compiler {
962
986
  };
963
987
 
964
988
  if let Some(ref mut logger) = self.logger {
965
- logger.log(&path, FileOperation::Compiled, &self.config.source);
989
+ logger.log(&path, FileOperation::Compiled, canonical_source);
966
990
 
967
991
  // Log each component occurrence (for counting)
968
992
  if let Some(all_srcs) = all_srcs {
@@ -990,6 +1014,7 @@ impl Compiler {
990
1014
  dir: &Path,
991
1015
  relative_path: &str,
992
1016
  canonical_output: &Path,
1017
+ canonical_source: &Path,
993
1018
  stats: &mut CompileStats,
994
1019
  ) -> Result<(), CompileError> {
995
1020
  let entries = fs::read_dir(dir).map_err(|e| CompileError::ReadError {
@@ -1027,7 +1052,7 @@ impl Compiler {
1027
1052
  } else {
1028
1053
  format!("{}/{}", relative_path, file_name)
1029
1054
  };
1030
- self.copy_directory(&path, &new_relative, stats)?;
1055
+ self.copy_directory(&path, &new_relative, canonical_source, stats)?;
1031
1056
  }
1032
1057
  // Skip further processing (don't recurse into components)
1033
1058
  continue;
@@ -1040,7 +1065,7 @@ impl Compiler {
1040
1065
  format!("{}/{}", relative_path, file_name)
1041
1066
  };
1042
1067
 
1043
- self.process_directory_assets_only(&path, &new_relative, canonical_output, stats)?;
1068
+ self.process_directory_assets_only(&path, &new_relative, canonical_output, canonical_source, stats)?;
1044
1069
  } else if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
1045
1070
  // Skip files matching skip patterns
1046
1071
  if should_skip_path(&path, file_name) {
@@ -1052,7 +1077,7 @@ impl Compiler {
1052
1077
  self.copy_file(&path, relative_path)?;
1053
1078
  stats.files_copied += 1;
1054
1079
  if let Some(ref mut logger) = self.logger {
1055
- logger.log(&path, FileOperation::Copied, &self.config.source);
1080
+ logger.log(&path, FileOperation::Copied, canonical_source);
1056
1081
  }
1057
1082
  }
1058
1083
  }
@@ -1095,12 +1120,17 @@ impl Compiler {
1095
1120
  );
1096
1121
 
1097
1122
  // Minify if requested
1098
- let output = if self.config.minify {
1123
+ let mut output = if self.config.minify {
1099
1124
  minify_html(&processed)
1100
1125
  } else {
1101
1126
  processed
1102
1127
  };
1103
1128
 
1129
+ // Remove FOUC prevention unless fouc_as_is is enabled
1130
+ if !self.config.fouc_as_is {
1131
+ output = remove_fouc_prevention(output);
1132
+ }
1133
+
1104
1134
  // Write to output
1105
1135
  let output_path = self.get_output_path(path, relative_path)?;
1106
1136
  fs::write(&output_path, output).map_err(|e| CompileError::WriteError {
@@ -1208,8 +1238,8 @@ impl Compiler {
1208
1238
  // Transform custom tags to <component> tags
1209
1239
  let transformed = parser.process_html(
1210
1240
  &content,
1211
- false, // elements_as_is
1212
- &[], // reserved_elements
1241
+ self.config.elements_as_is,
1242
+ &self.config.reserved_elements,
1213
1243
  true, // components_as_is (don't inline, just transform)
1214
1244
  &self.config.components,
1215
1245
  );
@@ -1245,8 +1275,8 @@ impl Compiler {
1245
1275
  // Transform custom tags to <component> tags
1246
1276
  let transformed = parser.process_html(
1247
1277
  &content,
1248
- false, // elements_as_is
1249
- &[], // reserved_elements
1278
+ self.config.elements_as_is,
1279
+ &self.config.reserved_elements,
1250
1280
  true, // components_as_is (don't inline, just transform)
1251
1281
  &self.config.components,
1252
1282
  );
@@ -1459,8 +1489,8 @@ impl Compiler {
1459
1489
  let file_name = entry.file_name();
1460
1490
  let file_name_str = file_name.to_string_lossy();
1461
1491
 
1462
- // Skip hidden files and specific directories/patterns
1463
- if file_name_str.starts_with('.') || should_skip_path(&path, &file_name_str) {
1492
+ // Skip specific directories/patterns (includes dotfiles via SKIP_FILES)
1493
+ if should_skip_path(&path, &file_name_str) {
1464
1494
  continue;
1465
1495
  }
1466
1496
 
@@ -1523,6 +1553,7 @@ impl Compiler {
1523
1553
  &mut self,
1524
1554
  dir: &Path,
1525
1555
  relative_path: &str,
1556
+ canonical_source: &Path,
1526
1557
  stats: &mut CompileStats,
1527
1558
  ) -> Result<(), CompileError> {
1528
1559
  let entries = fs::read_dir(dir).map_err(|e| CompileError::ReadError {
@@ -1541,12 +1572,12 @@ impl Compiler {
1541
1572
 
1542
1573
  if path.is_dir() {
1543
1574
  let new_relative = format!("{}/{}", relative_path, file_name);
1544
- self.copy_directory(&path, &new_relative, stats)?;
1575
+ self.copy_directory(&path, &new_relative, canonical_source, stats)?;
1545
1576
  } else {
1546
1577
  self.copy_file(&path, relative_path)?;
1547
1578
  stats.files_copied += 1;
1548
1579
  if let Some(ref mut logger) = self.logger {
1549
- logger.log(&path, FileOperation::Copied, &self.config.source);
1580
+ logger.log(&path, FileOperation::Copied, canonical_source);
1550
1581
  }
1551
1582
  }
1552
1583
  }