FrançaisPlayground

Component resolution

A capitalised tag with no binding in scope is a name the compiler can look up. Tell it where Button lives once, and every template can use <Button> without an import.

// No import anywhere in this file.
export function Toolbar() {
  return (
    <Stack gap="3">
      <Button>Save</Button>
    </Stack>
  );
}

This is not auto-import in the editor sense — nothing is written into your source. The compiler injects the import while compiling, so the output is exactly what you would have written by hand, and the bundler sees a normal static import.

Fluixi already did this for Show, For, Portal and the router components. resolve is the same mechanism, opened up.

Configuring it

Rules live on the plugin, because where a component comes from is the same everywhere in a project:

// vite.config.ts
import { fluixi } from '@fluixi/vite-plugin';

export default defineConfig({
  plugins: [
    fluixi({
      resolve: [
        { components: { Button: '@ui/button', Stack: '@ui/layout' } },
      ],
    }),
  ],
});

A component library ships its own map, so adopting it is one line:

import { uiComponents } from '@fluixi-ui/resolver';

fluixi({ resolve: [uiComponents()] });

When the tag and the export differ

resolve: [
  { components: { Btn: { module: '@ui/button', export: 'Button' } } },
  // `export: 'default'` for a file-per-component layout
  { components: { Card: { module: './Card.js', export: 'default' } } },
];

Patterns

One rule can cover a family. $1 and friends are substituted from the match:

resolve: [{ match: /^Icon(.+)$/, module: '@/icons/$1' }];

<IconTrash> resolves to @/icons/Trash. Note the limitation below — patterns cannot be declared for TypeScript.

An import always wins

Writing the import yourself shadows the rule. Resolution only fills in names that have no binding in scope, so there is never a conflict to resolve, and moving a component out of the config is safe.

Telling TypeScript

The import only exists after compilation, so without help TypeScript reports Cannot find name 'Button' for every resolved tag — the code runs, the editor is unusable.

The plugin generates the declarations for you. On dev-server start and on build it writes fluixi.d.ts at the project root:

// GENERATED by @fluixi/compiler — do not edit.
import * as __ui_button from '@ui/button';

declare global {
  export import Button = __ui_button.Button;
}

Commit this file. It is what makes a fresh clone type-check before anyone runs the dev server, and it means a CI job that type-checks without building still works.

It has to be covered by include in tsconfig.json, which for the default location means naming it alongside src:

{ "include": ["src", "fluixi.d.ts"] }

Scaffolded projects already have that line. If the file lands somewhere TypeScript will not read it, the plugin says so during the build — an ignored file and a missing one produce identical errors, so it is worth being told which one you have.

Put it elsewhere, or turn it off, with globals:

fluixi({ resolve: [...], globals: 'types/fluixi.d.ts' });
fluixi({ resolve: [...], globals: false });

What the generated file leaves out

  • Names lib.dom already claims. An ambient const Text cannot beat the DOM's own Text, so those are skipped and listed in the header — import them explicitly.
  • Components from packages you do not depend on. Declaring one produces an unresolvable import, which under skipLibCheck fails silently and degrades the component to any — worse than the error it replaced.
  • Pattern rules. A pattern matches unboundedly many names, so there is no list to emit. Those tags compile, but need a hand-written declaration for the editor.

In the editor

@fluixi/ts-plugin resolves the same names, so hover, go-to-definition and rename work on a resolved tag exactly as they would on an imported one — including inside html `` templates, where the tag is only text as far as TypeScript is concerned.

Because the declarations use an alias (export import Button = …) rather than a typed constant, hover keeps the real signature and its JSDoc, and go-to-definition lands on the component's source rather than on the generated file.