@ape-egg/vibe 1.0.5 → 1.1.2

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +228 -23
  3. package/compiler/bin/vibe-compile.js +109 -0
  4. package/compiler/native/.gitkeep +0 -0
  5. package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
  6. package/compiler/src/Cargo.lock +1885 -0
  7. package/compiler/src/Cargo.toml +29 -0
  8. package/compiler/src/compiler/compile.rs +1209 -0
  9. package/compiler/src/compiler/mod.rs +5 -0
  10. package/compiler/src/config.rs +184 -0
  11. package/compiler/src/main.rs +284 -0
  12. package/compiler/src/parser/element.rs +96 -0
  13. package/compiler/src/parser/html.rs +335 -0
  14. package/compiler/src/parser/mod.rs +8 -0
  15. package/index.js +2 -248
  16. package/package.json +26 -3
  17. package/{affected.js → runtime/affected.js} +64 -4
  18. package/runtime/cleanup.js +59 -0
  19. package/runtime/component.js +116 -0
  20. package/{conditionals.js → runtime/conditionals.js} +25 -11
  21. package/{constants.js → runtime/constants.js} +23 -3
  22. package/runtime/debug.js +91 -0
  23. package/{hydrate.js → runtime/hydrate.js} +57 -7
  24. package/runtime/index.js +614 -0
  25. package/{iterate.js → runtime/iterate.js} +53 -45
  26. package/{iteration-utils.js → runtime/iteration-utils.js} +11 -1
  27. package/{parse.js → runtime/parse.js} +37 -7
  28. package/runtime/state.js +52 -0
  29. package/ROADMAP.md +0 -289
  30. package/llms.txt +0 -279
  31. package/state.js +0 -26
  32. /package/{_vibe-compiled-iteration-batch.js → runtime/_vibe-compiled-iteration-batch.js} +0 -0
  33. /package/{link.js → runtime/manifest.js} +0 -0
  34. /package/{utils.js → runtime/utils.js} +0 -0
  35. /package/{vibe.css → runtime/vibe.css} +0 -0
@@ -0,0 +1,335 @@
1
+ use std::fs;
2
+ use std::path::Path;
3
+ use std::collections::{HashMap, HashSet};
4
+ use thiserror::Error;
5
+
6
+ use super::element::{Element, ElementCache};
7
+
8
+ #[derive(Error, Debug)]
9
+ pub enum ParseError {
10
+ #[error("Failed to read file {path}: {source}")]
11
+ ReadError {
12
+ path: String,
13
+ #[source]
14
+ source: std::io::Error,
15
+ },
16
+ #[error("Element not found: {0}")]
17
+ #[allow(dead_code)]
18
+ ElementNotFound(String),
19
+ }
20
+
21
+ pub struct HtmlParser {
22
+ components_dir: std::path::PathBuf,
23
+ cache: ElementCache,
24
+ }
25
+
26
+ impl HtmlParser {
27
+ pub fn new(components_dir: std::path::PathBuf) -> Self {
28
+ Self {
29
+ components_dir,
30
+ cache: ElementCache::new(),
31
+ }
32
+ }
33
+
34
+ /// Load all elements from the elements directory
35
+ pub fn load_elements(&mut self) -> Result<(), ParseError> {
36
+ if !self.components_dir.exists() {
37
+ return Ok(()); // No elements directory is fine
38
+ }
39
+
40
+ self.load_elements_recursive(&self.components_dir.clone())
41
+ }
42
+
43
+ fn load_elements_recursive(&mut self, dir: &Path) -> Result<(), ParseError> {
44
+ let entries = fs::read_dir(dir).map_err(|e| ParseError::ReadError {
45
+ path: dir.display().to_string(),
46
+ source: e,
47
+ })?;
48
+
49
+ for entry in entries.flatten() {
50
+ let path = entry.path();
51
+
52
+ if path.is_dir() {
53
+ self.load_elements_recursive(&path)?;
54
+ } else if path.extension().map_or(false, |ext| ext == "html") {
55
+ let tag_name = path
56
+ .file_stem()
57
+ .and_then(|s| s.to_str())
58
+ .unwrap_or("")
59
+ .to_string();
60
+
61
+ let content = fs::read_to_string(&path).map_err(|e| ParseError::ReadError {
62
+ path: path.display().to_string(),
63
+ source: e,
64
+ })?;
65
+
66
+ let element = Element::new(tag_name.clone(), path.clone(), content);
67
+ self.cache.insert(tag_name, element);
68
+ }
69
+ }
70
+
71
+ Ok(())
72
+ }
73
+
74
+ /// Get an element by tag name
75
+ pub fn _get_element(&self, tag_name: &str) -> Option<&Element> {
76
+ self.cache.get(tag_name)
77
+ }
78
+
79
+ /// Get all loaded elements
80
+ #[allow(dead_code)]
81
+ pub fn elements(&self) -> &ElementCache {
82
+ &self.cache
83
+ }
84
+
85
+ /// Process HTML content: transform custom tags to <component>, optionally inline, apply accessibility
86
+ pub fn process_html(
87
+ &self,
88
+ content: &str,
89
+ accessibility: bool,
90
+ exclude_tags: &[String],
91
+ elements_as_is: bool,
92
+ components_dir: &str,
93
+ ) -> String {
94
+ self.process_html_with_cache(content, accessibility, exclude_tags, elements_as_is, components_dir, &HashMap::new())
95
+ }
96
+
97
+ pub fn process_html_with_cache(
98
+ &self,
99
+ content: &str,
100
+ accessibility: bool,
101
+ exclude_tags: &[String],
102
+ elements_as_is: bool,
103
+ components_dir: &str,
104
+ external_cache: &HashMap<String, String>,
105
+ ) -> String {
106
+ let mut result = content.to_string();
107
+
108
+ // Step 1: Transform custom tags matching element files to <component> tags
109
+ result = self.transform_custom_tags_to_component(&result, components_dir);
110
+
111
+ // Step 2: If elements_as_is is false, recursively inline all <component> elements
112
+ if !elements_as_is {
113
+ result = self.inline_component_elements(&result, external_cache);
114
+ }
115
+
116
+ // Step 3: Accessibility transform if requested
117
+ if accessibility {
118
+ result = transform_custom_tags_to_divs(&result, exclude_tags);
119
+ }
120
+
121
+ result
122
+ }
123
+
124
+ /// Transform custom tags that match element files to <component src="..."> tags
125
+ fn transform_custom_tags_to_component(&self, content: &str, components_dir: &str) -> String {
126
+ let mut result = content.to_string();
127
+
128
+ // Find all tags that match loaded elements
129
+ for tag_name in self.cache.keys() {
130
+ // Match opening and closing tags with any attributes and children
131
+ let tag_pattern = format!(r"<{}(\s[^>]*)?>", regex::escape(tag_name));
132
+ let tag_re = regex::Regex::new(&tag_pattern).unwrap();
133
+ let closing_pattern = format!(r"</{}>", regex::escape(tag_name));
134
+
135
+ // Find all occurrences and transform them
136
+ let mut matches: Vec<(usize, usize, String, Option<String>)> = Vec::new();
137
+
138
+ // Find opening tags
139
+ for cap in tag_re.find_iter(&result) {
140
+ let start = cap.start();
141
+ let tag_with_attrs = cap.as_str();
142
+
143
+ // Extract attributes (everything between tag name and >)
144
+ let attrs = if tag_with_attrs.ends_with('>') {
145
+ let inner = &tag_with_attrs[tag_name.len() + 1..tag_with_attrs.len() - 1];
146
+ if inner.trim().is_empty() {
147
+ None
148
+ } else {
149
+ Some(inner.to_string())
150
+ }
151
+ } else {
152
+ None
153
+ };
154
+
155
+ // Find corresponding closing tag
156
+ if let Some(closing_pos) = result[cap.end()..].find(&closing_pattern) {
157
+ let closing_start = cap.end() + closing_pos;
158
+ let closing_end = closing_start + closing_pattern.len();
159
+ let children = result[cap.end()..closing_start].to_string();
160
+
161
+ matches.push((start, closing_end, children, attrs));
162
+ }
163
+ }
164
+
165
+ // Replace from end to start to maintain indices
166
+ matches.reverse();
167
+ for (start, end, children, attrs) in matches {
168
+ let attrs_str = attrs.map(|a| format!(" {}", a)).unwrap_or_default();
169
+ let replacement = format!("<component src=\"/{}/{}.html\"{}>{}</component>", components_dir, tag_name, attrs_str, children);
170
+ result.replace_range(start..end, &replacement);
171
+ }
172
+ }
173
+
174
+ result
175
+ }
176
+
177
+ /// Recursively inline all <component> elements with their HTML content
178
+ fn inline_component_elements(&self, content: &str, external_cache: &HashMap<String, String>) -> String {
179
+ let mut result = content.to_string();
180
+ let mut changed = true;
181
+ let mut iterations = 0;
182
+ const MAX_ITERATIONS: usize = 100; // Prevent infinite loops
183
+
184
+ while changed && iterations < MAX_ITERATIONS {
185
+ changed = false;
186
+ iterations += 1;
187
+
188
+ // Match <component src="..." attrs...>children</component>
189
+ // Handles internal (/components/file.html) and external (http://... or https://...)
190
+ // Use (?s) flag to make . match newlines
191
+ let component_re = regex::Regex::new(
192
+ r#"(?s)<component\s+src="([^"]+)"([^>]*)>(.*?)</component>"#
193
+ ).unwrap();
194
+
195
+ let matches: Vec<_> = component_re.captures_iter(&result).map(|cap| {
196
+ let full_match = cap.get(0).unwrap();
197
+ let src = cap.get(1).unwrap().as_str();
198
+ let attrs_str = cap.get(2).map(|m| m.as_str().to_string()).unwrap_or_default();
199
+ let slot_content = cap.get(3).map(|m| m.as_str().to_string());
200
+
201
+ // Parse attributes into a map (prop_name -> prop_value)
202
+ let props = Self::parse_props(&attrs_str);
203
+
204
+ (full_match.start(), full_match.end(), src.to_string(), props, slot_content)
205
+ }).collect();
206
+
207
+ if !matches.is_empty() {
208
+ changed = true;
209
+ }
210
+
211
+ // Replace from end to start
212
+ for (start, end, src, props, slot_content) in matches.iter().rev() {
213
+ // Check if it's an external URL
214
+ let replacement_content = if src.starts_with("http://") || src.starts_with("https://") {
215
+ // External component - get from cache
216
+ external_cache.get(src).cloned()
217
+ } else {
218
+ // Internal component - get element name from filename (e.g., "card.html" -> "card")
219
+ let element_name = src
220
+ .trim_start_matches("./")
221
+ .trim_start_matches('/')
222
+ .split('/')
223
+ .last()
224
+ .unwrap_or(src)
225
+ .trim_end_matches(".html");
226
+
227
+ self.cache.get(element_name).map(|e| e.content.clone())
228
+ };
229
+
230
+ if let Some(mut replacement) = replacement_content {
231
+ // Replace props: for each prop like headline="@[pageTitle]",
232
+ // replace @[headline] in content with @[pageTitle]
233
+ for (prop_name, prop_value) in props {
234
+ let prop_binding = format!("@[{}]", prop_name);
235
+ replacement = replacement.replace(&prop_binding, prop_value);
236
+ }
237
+
238
+ // Replace <slot></slot> with slot content if present
239
+ if let Some(ref slot) = slot_content {
240
+ if !slot.trim().is_empty() {
241
+ replacement = replacement.replace("<slot></slot>", slot);
242
+ replacement = replacement.replace("<slot/>", slot);
243
+ replacement = replacement.replace("<slot />", slot);
244
+ }
245
+ }
246
+
247
+ result.replace_range(*start..*end, &replacement);
248
+ }
249
+ // If not found in cache or internal elements, leave as-is
250
+ }
251
+ }
252
+
253
+ result
254
+ }
255
+
256
+ /// Parse component props from attributes string
257
+ /// Example: ` headline="@[pageTitle]" theme="dark"` -> {"headline": "@[pageTitle]", "theme": "dark"}
258
+ fn parse_props(attrs_str: &str) -> HashMap<String, String> {
259
+ let mut props = HashMap::new();
260
+
261
+ // Match attribute="value" pairs
262
+ let attr_re = regex::Regex::new(r#"(\w+)="([^"]*)""#).unwrap();
263
+
264
+ for cap in attr_re.captures_iter(attrs_str) {
265
+ if let (Some(name), Some(value)) = (cap.get(1), cap.get(2)) {
266
+ props.insert(name.as_str().to_string(), value.as_str().to_string());
267
+ }
268
+ }
269
+
270
+ props
271
+ }
272
+ }
273
+
274
+ /// Transform custom HTML elements to divs with classes
275
+ fn transform_custom_tags_to_divs(content: &str, exclude_tags: &[String]) -> String {
276
+ let mut result = content.to_string();
277
+
278
+ // Standard HTML5 elements (should not be transformed)
279
+ let standard_tags: HashSet<&str> = [
280
+ "a", "abbr", "address", "area", "article", "aside", "audio",
281
+ "b", "base", "bdi", "bdo", "blockquote", "body", "br", "button",
282
+ "canvas", "caption", "cite", "code", "col", "colgroup",
283
+ "data", "datalist", "dd", "del", "details", "dfn", "dialog", "div", "dl", "dt",
284
+ "em", "embed",
285
+ "fieldset", "figcaption", "figure", "footer", "form",
286
+ "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html",
287
+ "i", "iframe", "img", "input", "ins",
288
+ "kbd",
289
+ "label", "legend", "li", "link",
290
+ "main", "map", "mark", "menu", "meta", "meter",
291
+ "nav", "noscript",
292
+ "object", "ol", "optgroup", "option", "output",
293
+ "p", "param", "picture", "pre", "progress",
294
+ "q",
295
+ "rp", "rt", "ruby",
296
+ "s", "samp", "script", "search", "section", "select", "slot", "small", "source", "span", "strong", "style", "sub", "summary", "sup", "svg",
297
+ "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track",
298
+ "u", "ul",
299
+ "var", "video",
300
+ "wbr",
301
+ ].into_iter().collect();
302
+
303
+ // Find all custom tags and transform them
304
+ let tag_pattern = regex::Regex::new(r"<([a-z][a-z0-9-]*)([^>]*)>").unwrap();
305
+ let _closing_pattern = regex::Regex::new(r"</([a-z][a-z0-9-]*)>").unwrap();
306
+
307
+ // Collect unique custom tags first
308
+ let mut custom_tags: Vec<String> = Vec::new();
309
+ for cap in tag_pattern.captures_iter(&result.clone()) {
310
+ if let Some(m) = cap.get(1) {
311
+ let tag = m.as_str().to_string();
312
+ if !standard_tags.contains(tag.as_str())
313
+ && !exclude_tags.contains(&tag)
314
+ && !custom_tags.contains(&tag)
315
+ {
316
+ custom_tags.push(tag);
317
+ }
318
+ }
319
+ }
320
+
321
+ // Transform each custom tag
322
+ for tag in custom_tags {
323
+ // Opening tag: <custom-tag attrs> -> <div class="custom-tag" attrs>
324
+ let open_re = regex::Regex::new(&format!(r"<{}([^>]*)>", regex::escape(&tag))).unwrap();
325
+ result = open_re
326
+ .replace_all(&result, format!("<div class=\"{}\"$1>", tag).as_str())
327
+ .to_string();
328
+
329
+ // Closing tag: </custom-tag> -> </div>
330
+ let close_re = regex::Regex::new(&format!(r"</{}>", regex::escape(&tag))).unwrap();
331
+ result = close_re.replace_all(&result, "</div>").to_string();
332
+ }
333
+
334
+ result
335
+ }
@@ -0,0 +1,8 @@
1
+ pub mod html;
2
+ mod element;
3
+
4
+ pub use html::HtmlParser;
5
+ #[allow(unused_imports)]
6
+ pub use html::ParseError;
7
+ #[allow(unused_imports)]
8
+ pub use element::Element;
package/index.js CHANGED
@@ -1,248 +1,2 @@
1
- import state from './state.js';
2
- import parse from './parse.js';
3
- import link from './link.js';
4
- import hydrate, { setPreviousState } from './hydrate.js';
5
- import affected from './affected.js';
6
- import { deepMerge, hash } from './utils.js';
7
- import { renderAllIterations, setRenderAllConditionals } from './iterate.js';
8
- import { renderAllConditionals } from './conditionals.js';
9
- import { NON_REACTIVE_ELEMENTS } from './constants.js';
10
-
11
- // Wire up cross-module dependency after all modules are loaded
12
- setRenderAllConditionals(renderAllConditionals);
13
-
14
- // Check if node itself or any ancestor is non-reactive or dehydrated
15
- const isNonReactiveOrInside = (node) => {
16
- let current = node;
17
- while (current && current !== document.body) {
18
- if (NON_REACTIVE_ELEMENTS.includes(current.nodeName)) {
19
- return true;
20
- }
21
- if (current.hasAttribute?.('dehydrate')) {
22
- return true;
23
- }
24
- current = current.parentElement;
25
- }
26
- return false;
27
- };
28
-
29
- // Navigate tree using dot notation (handles .children at each level)
30
- const navigateTree = (tree, path) => {
31
- if (!path) return tree;
32
- return path.split('.').reduce((node, key) => node?.children?.[key], tree);
33
- };
34
-
35
- // Get or create a node in the tree at the given path
36
- const ensureNode = (tree, path) => {
37
- const keys = path.split('.');
38
- return keys.reduce((node, key) => {
39
- if (!node.children[key]) {
40
- node.children[key] = { children: {} };
41
- }
42
- return node.children[key];
43
- }, tree);
44
- };
45
-
46
- const main = (s, attrName = 'vibe') => {
47
- // Find element(s) with the specified attribute
48
- const elements = document.querySelectorAll(`[${attrName}]`);
49
-
50
- if (elements.length === 0) {
51
- console.info(`[vibe] No element found with attribute "${attrName}". Falling back to body.`);
52
- } else if (elements.length > 1) {
53
- console.info(
54
- `[vibe] Multiple elements (${elements.length}) found with attribute "${attrName}". Hydrating the first one.`,
55
- );
56
- }
57
-
58
- const rootElement = elements[0] || document.body;
59
- let parsedTree = parse(rootElement);
60
- let linkList = link(parsedTree);
61
-
62
- // Store previous state for comparison
63
- let previousState = {};
64
-
65
- // Lifecycle hooks that users can subscribe to
66
- const hooks = {
67
- afterUpdate: [],
68
- afterDomMutation: [],
69
- };
70
-
71
- // Observer reference and callback - defined here so state handler can access processMutations
72
- let observer = null;
73
- let processMutations = null;
74
-
75
- const $ = state(s, (newState) => {
76
- const mergedState = deepMerge($, newState);
77
- const affectedElements = affected(parsedTree, previousState, mergedState);
78
-
79
- // Capture pending mutations before disconnecting (takeRecords clears the queue)
80
- let pendingMutations = [];
81
- if (observer) {
82
- pendingMutations = observer.takeRecords();
83
- observer.disconnect();
84
- }
85
-
86
- hydrate(affectedElements, mergedState, linkList);
87
-
88
- if (observer) {
89
- observer.observe(rootElement, {
90
- attributes: false,
91
- characterData: false,
92
- childList: true,
93
- subtree: true,
94
- });
95
-
96
- // Process mutations that were pending before we disconnected
97
- if (pendingMutations.length > 0 && processMutations) {
98
- processMutations(pendingMutations);
99
- }
100
- }
101
-
102
- const prev = structuredClone(previousState);
103
- previousState = { ...$, ...newState };
104
- hooks.afterUpdate.forEach((callback) => callback(structuredClone({ ...$ }), prev));
105
- });
106
-
107
- // Add hook subscription method (non-enumerable so it won't be spread/cloned with state)
108
- Object.defineProperty($, 'on', {
109
- value: (event, callback) => {
110
- if (hooks[event]) {
111
- hooks[event].push(callback);
112
- }
113
- return () => (hooks[event] = hooks[event].filter((cb) => cb !== callback));
114
- },
115
- enumerable: false,
116
- });
117
-
118
- // Initial hydration
119
- const affectedElements = affected(parsedTree, $, $);
120
- hydrate(affectedElements, $, linkList);
121
-
122
- // Render all iterations and conditionals after initial hydration
123
- setPreviousState($);
124
- previousState = { ...$ };
125
- renderAllIterations(parsedTree, $, linkList);
126
- renderAllConditionals(parsedTree, $, linkList);
127
-
128
- // Define observer callback as named function so we can call it manually for pending mutations
129
- processMutations = (mutations) => {
130
- // Early exit if no mutations to process (common case)
131
- if (mutations.length === 0) return;
132
-
133
- let hadChanges = false;
134
- let parsedParents = null; // Lazy init - only create Set when needed
135
-
136
- mutations.forEach(({ addedNodes, removedNodes, target }) => {
137
- removedNodes.forEach((node) => {
138
- const entry = Object.entries(linkList).find(([_, element]) => element === node);
139
-
140
- // Skip nodes that aren't tracked (e.g., iteration-generated nodes or nodes outside reactive scope)
141
- if (!entry) return;
142
-
143
- const [dotAnnotation] = entry;
144
- delete linkList[dotAnnotation];
145
-
146
- const dotPath = dotAnnotation.split('.');
147
- const name = dotPath.pop();
148
- const parentDotAnnotation = dotPath.join('.');
149
-
150
- const picked = navigateTree(parsedTree, parentDotAnnotation);
151
-
152
- // If we can't navigate to the parent, skip
153
- if (!picked || !picked.element) return;
154
-
155
- // Update parent's parsed HTML (only once per parent)
156
- if (!parsedParents) parsedParents = new Set();
157
- if (!parsedParents.has(picked)) {
158
- const { parsed } = parse(picked.element);
159
- picked.parsed = parsed;
160
- parsedParents.add(picked);
161
- }
162
-
163
- // Remove the node from parent's children
164
- delete picked.children[name];
165
-
166
- hadChanges = true;
167
- });
168
-
169
- addedNodes.forEach((node) => {
170
- // Skip if node itself or any ancestor is non-reactive
171
- if (isNonReactiveOrInside(node)) {
172
- return;
173
- }
174
-
175
- const entry = Object.entries(linkList).find(([_, element]) => element === target);
176
-
177
- // If parent isn't tracked, this node is outside the reactive scope
178
- if (!entry) return;
179
-
180
- const [dotAnnotation] = entry;
181
- const picked = navigateTree(parsedTree, dotAnnotation);
182
-
183
- // If we can't navigate to the parent in the tree, skip
184
- if (!picked) return;
185
-
186
- // If parent has no element reference, re-parse from the actual DOM element
187
- if (!picked.element) {
188
- picked.element = target;
189
- }
190
-
191
- // Parse the newly added node
192
- const name = `${node.nodeName.toLowerCase()}_${hash()}`;
193
- const parsedNode = parse(node);
194
-
195
- // Update parent's parsed HTML (only once per parent)
196
- if (!parsedParents) parsedParents = new Set();
197
- if (!parsedParents.has(picked)) {
198
- const { parsed } = parse(picked.element);
199
- picked.parsed = parsed;
200
- parsedParents.add(picked);
201
- }
202
-
203
- // Add the parsed node to parent's children
204
- picked.children[name] = parsedNode;
205
-
206
- linkList[`${dotAnnotation}.${name}`] = node;
207
-
208
- // Only hydrate the newly added node and its descendants, not the entire tree
209
- // Use empty object as "previous state" so all bindings in new node are considered affected
210
- const affectedElements = affected(parsedNode, {}, $);
211
-
212
- hydrate(affectedElements, $, linkList);
213
-
214
- // Process iterations and conditionals in the newly added node
215
- renderAllIterations(parsedNode, $, linkList);
216
- renderAllConditionals(parsedNode, $, linkList);
217
-
218
- hadChanges = true;
219
- });
220
- });
221
-
222
- // Fire hooks once after all mutations are processed (not per-node)
223
- if (hadChanges) {
224
- hooks.afterDomMutation.forEach((callback) => callback());
225
- }
226
- };
227
-
228
- observer = new MutationObserver(processMutations);
229
-
230
- observer.observe(rootElement, {
231
- attributes: false,
232
- characterData: false,
233
- childList: true,
234
- subtree: true,
235
- attributeOldValue: false,
236
- characterDataOldValue: false,
237
- });
238
-
239
- // Force reflow - ensures layout is applied before transitions re-enable
240
- rootElement.offsetHeight;
241
-
242
- // Remove the vibe attribute to reveal content and enable transitions
243
- rootElement.removeAttribute(attrName);
244
-
245
- return $;
246
- };
247
-
248
- export default main;
1
+ // Re-export from runtime for backwards compatibility
2
+ export { default } from './runtime/index.js';
package/package.json CHANGED
@@ -1,10 +1,32 @@
1
1
  {
2
2
  "name": "@ape-egg/vibe",
3
- "version": "1.0.5",
3
+ "version": "1.1.2",
4
4
  "type": "module",
5
- "description": "Runtime-first reactivity",
5
+ "description": "Runtime-first reactivity with optional compiler",
6
6
  "main": "index.js",
7
7
  "homepage": "https://vibe.korte.kim",
8
+ "exports": {
9
+ ".": "./index.js",
10
+ "./runtime": "./runtime/index.js",
11
+ "./compiler": "./compiler/bin/vibe-compile.js"
12
+ },
13
+ "bin": {
14
+ "vibe": "./compiler/bin/vibe-compile.js"
15
+ },
16
+ "files": [
17
+ "index.js",
18
+ "runtime/",
19
+ "compiler/bin/",
20
+ "compiler/native/",
21
+ "compiler/src/Cargo.lock",
22
+ "compiler/src/Cargo.toml",
23
+ "compiler/src/compiler/",
24
+ "compiler/src/config.rs",
25
+ "compiler/src/main.rs",
26
+ "compiler/src/parser/",
27
+ "README.md",
28
+ "CHANGELOG.md"
29
+ ],
8
30
  "keywords": [
9
31
  "reactive",
10
32
  "framework",
@@ -12,7 +34,8 @@
12
34
  "ui",
13
35
  "mutation-observer",
14
36
  "proxy",
15
- "minimalistic"
37
+ "minimalistic",
38
+ "compiler"
16
39
  ],
17
40
  "scripts": {
18
41
  "test": "echo \"Error: no test specified\" && exit 1"