@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 +62 -1
- package/README.md +136 -24
- package/boot.js +11 -0
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/src/Cargo.lock +1 -1
- package/compiler/src/Cargo.toml +1 -1
- package/compiler/src/compiler/compile.rs +53 -22
- package/compiler/src/compiler/component_tagger.rs +22 -3
- package/compiler/src/compiler/value_stamper.rs +75 -5
- package/compiler/src/compiler/watcher.rs +69 -2
- package/compiler/src/config.rs +10 -0
- package/compiler/src/main.rs +45 -18
- package/compiler/src/parser/html.rs +61 -9
- package/index.js +34 -5
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/runtime/cleanup.js +0 -9
- package/runtime/component.js +14 -18
- package/runtime/index.js +13 -2
- package/runtime/iterate.js +63 -15
- package/runtime/parse.js +3 -2
- package/runtime/pre-compiled-manifest.js +120 -53
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
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
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>
|
|
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>
|
|
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++;
|
|
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.
|
|
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
|
|
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
|
-
#
|
|
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 --
|
|
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 --
|
|
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
|
-
"
|
|
287
|
-
"
|
|
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 `
|
|
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
|
-
"
|
|
308
|
-
"
|
|
309
|
-
"
|
|
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
|
-
###
|
|
467
|
+
### Element Transformation
|
|
372
468
|
|
|
373
|
-
|
|
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
|
-
|
|
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
|
|
|
Binary file
|
package/compiler/src/Cargo.lock
CHANGED
package/compiler/src/Cargo.toml
CHANGED
|
@@ -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
|
|
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(&
|
|
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(&
|
|
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
|
|
825
|
+
let pre_rendered = stamper.stamp_html(html.to_string())?;
|
|
802
826
|
|
|
803
|
-
//
|
|
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,
|
|
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,
|
|
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
|
-
|
|
1212
|
-
&
|
|
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
|
-
|
|
1249
|
-
&
|
|
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
|
|
1463
|
-
if
|
|
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,
|
|
1580
|
+
logger.log(&path, FileOperation::Copied, canonical_source);
|
|
1550
1581
|
}
|
|
1551
1582
|
}
|
|
1552
1583
|
}
|