Back to flin
flin

FlinUI: Zero-Import Component System

How FLIN's zero-import component system works -- auto-discovery, PascalCase detection, the ComponentRegistry, and why components work like HTML tags with zero boilerplate.

Thales & Claude | March 25, 2026 9 min flin
flinflinuicomponentszero-import

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 <Button />. 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 <button> 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.

flin<button>Click me</button>    // HTML element (lowercase)
<Button>Click me</Button>    // FLIN component (uppercase)

<div class="container">      // HTML element
<Container>                   // FLIN component

<input type="text" />         // HTML element
<Input type="text" />         // 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:

rustpub 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.

rustpub struct ComponentRegistry {
    components: HashMap<String, CompiledComponent>,
    search_paths: Vec<PathBuf>,
}

When the renderer encounters <Button /> and does not find Button in the cache, it triggers a search:

rustimpl 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.flin

The implementation discovers these paths automatically:

rust// 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 parent
rustpub 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:

flin// Button.flin
label = props.label || "Click"
variant = props.variant || "default"
disabled = props.disabled || false

<button
    class="btn btn-{variant}"
    disabled={disabled}
    click={props.onClick}
>
    {label}
</button>
flin// 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

FrameworkImport/Setup RequiredLines
Reactimport React from 'react' + import Button from './Button' + export default App3+
Vueimport { ref } from 'vue' + import Button from './Button.vue' + export default { components: { Button } }3+
Svelteimport Button from './Button.svelte'1+
FLINNothing0

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

FrameworkStepsTime
Reactnpm init, npm install react, create component file, import, export, render5-10 minutes
Vuenpm init, npm install vue, create component file, import, register, use5-10 minutes
Sveltenpm init, npm install svelte, create component file, import, use2-5 minutes
FLINCreate .flin file, write <Button />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 <Button /> 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 browser

This 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 <MyWidget /> 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:

flin// 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)
<Card title={props.user.name}>
    <Avatar name={props.user.name} src={props.user.avatar} />
    <Text>{props.user.email}</Text>
    <Badge variant={props.user.role == "admin" ? "primary" : "default"}>
        {props.user.role}
    </Badge>
</Card>

// Page.flin (uses UserCard -- also auto-discovered)
{for user in users}
    <UserCard user={user} />
{/for}

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 <img> or <table> or <form>. You just used them. The browser knew what they were.

FLIN extends this to custom components. You do not import <Button> or <Card> or <Modal>. You just use them. The runtime knows where to find them.

The power of 2025 is everything behind the scenes: type-safe props, reactive rendering, scoped styles, hot reload, automatic discovery. But the developer experience is 1995: write tags, see results.


This is Part 81 of the "How We Built FLIN" series, documenting how a CEO in Abidjan and an AI CTO built a zero-import component system into a programming language.

Series Navigation: - [80] Error Tracking and Performance Monitoring - [81] FlinUI: Zero-Import Component System (you are here) - [82] From Zero to 70 Components in One Session - [83] FlinUI Complete: 365+ Components

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles