In React, before you can use a button component, you write import Button from './Button'. In Vue, you write import Button from './Button.vue' and then register it in the components object. In Svelte, you write import Button from './Button.svelte'. One line of boilerplate per component, per file. In a real application with 50 components per page, that is 50 import statements at the top of every file.
In FLIN, you write . That is it. No import. No export. No registration. No configuration. The component system discovers Button.flin automatically and renders it in place, exactly like is a built-in HTML tag.
Session 092 built this system -- the zero-import component architecture that makes FLIN feel like HTML with superpowers. This article explains exactly how it works, from the parser's PascalCase detection to the ComponentRegistry's search algorithm to the runtime's rendering pipeline.
The Core Rule: Uppercase Means Component
The entire zero-import system rests on a single parsing rule: any HTML-like tag that starts with an uppercase letter is a component. Any tag that starts with a lowercase letter is a standard HTML element.
<button>Click me</button> // HTML element (lowercase)
<Button>Click me</Button> // FLIN component (uppercase)// HTML element // FLIN component ```
This convention is borrowed from React and JSX, where it has been battle-tested by millions of developers. It is intuitive: HTML elements are lowercase (div, span, p, button), so uppercase names are unambiguously components.
The parser detects this distinction and sets a flag on the AST node:
pub struct ViewElement {
pub name: String, // "Button"
pub is_component: bool, // true (detected from PascalCase)
pub attributes: Vec<Attribute>,
pub children: Vec<ViewNode>,
}When is_component is true, the renderer looks up the component in the ComponentRegistry instead of emitting a raw HTML tag. When is_component is false, it emits the tag directly.
The ComponentRegistry: Discovery Without Configuration
The ComponentRegistry is the heart of the zero-import system. It maintains a cache of compiled components and a list of directories to search for .flin files.
pub struct ComponentRegistry {
components: HashMap<String, CompiledComponent>,
search_paths: Vec<PathBuf>,
}When the renderer encounters and does not find Button in the cache, it triggers a search:
impl ComponentRegistry {
pub fn find_component(&mut self, name: &str) -> Option<&CompiledComponent> {
// Check cache first
if self.components.contains_key(name) {
return self.components.get(name);
}// Search all paths for path in &self.search_paths { let exact = path.join(format!("{}.flin", name)); if exact.exists() { let compiled = self.compile_component(&exact)?; self.components.insert(name.to_string(), compiled); return self.components.get(name); }
let lower = path.join(format!("{}.flin", name.to_lowercase())); if lower.exists() { let compiled = self.compile_component(&lower)?; self.components.insert(name.to_string(), compiled); return self.components.get(name); } }
None } } ```
The search algorithm tries two filenames for each path: the exact name (Button.flin) and the lowercase variant (button.flin). It searches directories in order, returning the first match. This means local components (in the project's components/ directory) take priority over library components (in flinui/), enabling overrides without configuration.
Automatic Search Path Configuration
When you run flin dev app.flin, the runtime automatically configures search paths based on the project structure:
myapp/
app.flin -> search path: myapp/
components/ -> search path: myapp/components/
Header.flin
Footer.flin
flinui/ -> search path: myapp/flinui/
basic/ -> search path: myapp/flinui/basic/
Button.flin
Input.flin
layout/ -> search path: myapp/flinui/layout/
Container.flin
Grid.flin
forms/ -> search path: myapp/flinui/forms/
Form.flin
FormField.flin
feedback/ -> search path: myapp/flinui/feedback/
Alert.flin
Modal.flin
navigation/ -> search path: myapp/flinui/navigation/
Navbar.flin
Sidebar.flin
pro/
ai/ -> search path: myapp/flinui/pro/ai/
AIChatbot.flin
ChatInput.flinThe implementation discovers these paths automatically:
// In src/lib.rs
reg.add_search_path(base_path.to_path_buf());
reg.add_search_path(components_dir.clone());let flinui_dir = base_path.join("flinui"); if flinui_dir.exists() { reg.add_search_path(flinui_dir.clone());
let categories = [ "basic", "layout", "data", "forms", "feedback", "navigation", "charts", "typography", "collections", "templates", "flin-native", "theme", "pro" ];
for category in &categories { let category_path = flinui_dir.join(category); if category_path.exists() { reg.add_search_path(category_path.clone());
if *category == "pro" { let ai_path = category_path.join("ai"); if ai_path.exists() { reg.add_search_path(ai_path); } } } } } ```
Thirteen category directories, plus the base project directory and the user's components/ directory. Every .flin file in any of these directories is automatically available as a component, discoverable by name, without a single import statement.
How Component Rendering Works
When the VM encounters a component tag during rendering, it follows a four-step process:
1. Look up component in registry
2. Extract props from attributes
3. Render component with props
4. Insert rendered output into parentpub fn render_element_with_context(
element: &ViewElement,
vm: &mut Vm,
ctx: &mut RenderContext,
) -> Result<String, RenderError> {
if element.is_component {
// Step 1: Find the component
let component = ctx.registry
.find_component(&element.name)
.ok_or(RenderError::ComponentNotFound(element.name.clone()))?;// Step 2: Extract props let props = extract_props(&element.attributes, vm)?;
// Step 3: Render with new scope let output = render_component(component, props, vm)?;
// Step 4: Return HTML string return Ok(output); }
// Regular HTML element -- emit directly render_html_element(element, vm, ctx) } ```
Props: The Component Interface
Props are the data passed from parent to child through attributes. Inside a component file, props are accessed through the props object:
// Button.flin
label = props.label || "Click"
variant = props.variant || "default"
disabled = props.disabled || false```
// Usage
<Button label="Save" variant="primary" onClick={save()} />
<Button label="Cancel" />
<Button disabled={true}>Loading...</Button>The props object is a map populated from the component tag's attributes. props.label reads the label attribute. props.onClick reads the onClick attribute. The || operator provides default values for optional props.
Why This Is Revolutionary
The zero-import system is not just convenience. It fundamentally changes how developers think about components.
Comparison: Lines of Boilerplate Per File
| Framework | Import/Setup Required | Lines |
|---|---|---|
| React | import React from 'react' + import Button from './Button' + export default App | 3+ |
| Vue | import { ref } from 'vue' + import Button from './Button.vue' + export default { components: { Button } } | 3+ |
| Svelte | import Button from './Button.svelte' | 1+ |
| FLIN | Nothing | 0 |
In a file that uses 20 components (a typical dashboard page), the savings are substantial:
- React: 20+ import lines at the top of the file
- Svelte: 20 import lines
- FLIN: 0 import lines
The file starts immediately with logic and markup. No preamble. No ceremony. No boilerplate.
Time to First Component
| Framework | Steps | Time |
|---|---|---|
| React | npm init, npm install react, create component file, import, export, render | 5-10 minutes |
| Vue | npm init, npm install vue, create component file, import, register, use | 5-10 minutes |
| Svelte | npm init, npm install svelte, create component file, import, use | 2-5 minutes |
| FLIN | Create .flin file, write | 0 seconds |
The zero-import system means there is no setup step. The moment a .flin file exists in a search path, it is available everywhere. Drop Button.flin into your components/ directory, and every file in your project can use immediately.
No Dependency Hell
JavaScript's node_modules is the punchline of an industry-wide joke. A "Hello World" React application creates a folder with 1,200+ packages. Each package has its own dependencies, which have their own dependencies, which have their own dependencies. Version conflicts are common. Security vulnerabilities propagate through the dependency tree.
FLIN has no package manager. There are no dependencies to manage. A FlinUI component is a .flin file. It depends on other .flin files. There are no version numbers, no lock files, no resolution algorithms, and no supply chain attacks. The entire "dependency" system is the file system.
Hot Reload: Change a Component, See It Instantly
The ComponentRegistry supports hot reload. When a .flin file changes on disk, the registry invalidates its cache entry. The next time the component is referenced, it is recompiled from the modified source.
1. Developer edits Button.flin
2. File watcher detects change
3. Registry removes "Button" from cache
4. Next render triggers recompilation
5. Updated Button appears in the browserThis happens in under 50 milliseconds for most components. There is no rebuild step, no webpack recompilation, no module graph recalculation. Just: file changed, cache cleared, component recompiled on next use.
Error Handling: What Happens When a Component Is Not Found
If you write and there is no MyWidget.flin file in any search path, the renderer produces a clear error:
Component not found: MyWidget
Searched in:
./
./components/
./flinui/basic/
./flinui/layout/
./flinui/forms/
... (all search paths listed)Hint: Create a file named MyWidget.flin in any of these directories. ```
The error message tells you exactly what went wrong, where FLIN looked, and how to fix it. No cryptic "Module not found" error. No stack trace pointing to bundler internals. Just: "this component does not exist, here is where to put it."
Nested Components
Components can use other components without any special declaration:
// Card.flin
<div class="card">
<div class="card-header">{props.title}</div>
<div class="card-body">{props.children}</div>
</div>// UserCard.flin (uses Card, Avatar, Badge -- all auto-discovered)
// Page.flin (uses UserCard -- also auto-discovered)
{for user in users}
UserCard.flin uses Card, Avatar, Text, and Badge without importing them. Page.flin uses UserCard without importing it. The registry resolves each component the first time it is referenced, caches it, and reuses the cached version for subsequent references.
There is no limit to nesting depth. A component can use components that use components that use components. The registry handles circular references by detecting cycles during compilation and reporting a clear error.
The FLIN Philosophy Made Concrete
The zero-import component system is the purest expression of FLIN's core philosophy: "Write apps like 1995. With the power of 2025."
In 1995, HTML had no import system. You did not import or