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.domalready claims. An ambientconst Textcannot beat the DOM's ownText, 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
skipLibCheckfails silently and degrades the component toany— 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.