@polarityio/vite-plugin-icl
v1.0.1
Published
Vite plugin for the Polarity integration framework – transforms web component names into unique names.
Maintainers
Readme
@polarityio/vite-plugin-icl
A Vite plugin for the Polarity integration framework that automatically transforms web component names into globally unique, versioned names at build time — and handles component discovery, registration, and bundling automatically.
Write clean, readable component names in your source:
<key-value .label=${"Host"} .value=${host}></key-value>They are transformed into collision-proof names during the build:
<px-int-3f8kzq2m1v-icl-key-value-v1-0-0 .label=${"Host"} .value=${host}></px-int-3f8kzq2m1v-icl-key-value-v1-0-0>Table of Contents
- Requirements
- Installation
- Quick Start
- Adding a Component
- Exporting Additional Files
- Opting Out of Auto-Import
- Acronym Configuration
- System Components
- Library Component Aliases
- Custom Library Components
- Component Registries
- Plugin Options
- Contributing
Requirements
- Node.js ≥ 24
- Vite ≥ 5
Installation
npm install --save-dev @polarityio/vite-plugin-iclQuick Start
Point build.lib.entry at VIRTUAL_COMPONENTS_ID and tell the plugin where your component files live. Everything else is automatic.
// vite.config.ts
import { defineConfig } from 'vite';
import { resolve } from 'node:path';
import { transformComponentNames, VIRTUAL_COMPONENTS_ID } from '@polarityio/vite-plugin-icl';
export default defineConfig({
plugins: [
transformComponentNames({
componentsDir: resolve(__dirname, 'src/web-components'),
}),
],
build: {
lib: {
entry: VIRTUAL_COMPONENTS_ID,
formats: ['es'],
fileName: () => 'components.js',
},
},
});That's it. The plugin will:
- Scan
src/web-componentsfor every.tsfile - Derive a unique tag name for each component from its filename
- Verify each file exports the expected class
- Rewrite tag names in your templates at build time
- Inject
customElements.define(...)into each component file automatically - Bundle everything into a single output file
Adding a Component
Create a .ts file anywhere inside componentsDir. The filename becomes the component's tag name:
| File | Tag name |
|---|---|
| key-value.ts | <key-value> |
| save-modal.ts | <save-modal> |
| modals/confirm.ts | <modals--confirm> |
Each file must export a class named in PascalCase with a Component suffix. The build will fail with a clear error if the class is missing or misnamed.
// src/web-components/key-value.ts
import { LitElement, html } from 'lit';
import { property } from 'lit/decorators.js';
export class KeyValueComponent extends LitElement {
@property() label = '';
@property() value = '';
render() {
return html`
<dt>${this.label}</dt>
<dd>${this.value}</dd>
`;
}
}Naming rules: filenames must start with a lowercase letter and contain only lowercase letters, digits, hyphens, periods, or underscores. Names reserved by the HTML spec (
annotation-xml,color-profile, etc.) are not allowed.
Nested components
Files in subdirectories are supported. The directory path is included in the tag name, with path separators replaced by -- (double-hyphen):
src/web-components/
key-value.ts → <key-value> → KeyValueComponent
modals/
save-modal.ts → <modals--save-modal> → ModalsSaveModalComponentExporting Additional Files
If your library also exports utilities, types, or constants alongside your components, keep a hand-written index.ts for those exports and pass both entries to Vite. Rollup produces one output file per entry.
// vite.config.ts
import { defineConfig } from 'vite';
import { resolve } from 'node:path';
import { transformComponentNames, VIRTUAL_COMPONENTS_ID } from '@polarityio/vite-plugin-icl';
export default defineConfig({
plugins: [
transformComponentNames({
componentsDir: resolve(__dirname, 'src/web-components'),
additionalEntry: resolve(__dirname, 'src/index.ts'),
}),
],
build: {
lib: {
entry: [VIRTUAL_COMPONENTS_ID, resolve(__dirname, 'src/index.ts')],
formats: ['es'],
fileName: (_format, entryName) => {
if (entryName === 'virtual-icl-components') return 'components.js';
return 'index.js';
},
},
},
});This produces:
dist/
components.js ← auto-discovered web components
index.js ← your custom exportssrc/index.ts can export anything — the plugin does not transform it:
// src/index.ts
export { version } from './version.js';
export type { MyConfig } from './types.js';When using two entry points,
fileNamemust return a distinct name for each entry to prevent one file from overwriting the other.
Opting Out of Auto-Import
By default the plugin serves the VIRTUAL_COMPONENTS_ID virtual entry that automatically pulls every component into the bundle. Set autoImport: false to disable this and manage imports yourself via a hand-written index.ts.
transformComponentNames({
componentsDir: resolve(__dirname, 'src/web-components'),
autoImport: false,
})With autoImport: false:
VIRTUAL_COMPONENTS_IDis no longer available as a build entry- Your
vite.config.tsentry should point at your ownindex.ts - Tag rewriting and
customElements.define(...)injection still happen automatically for any component file that is imported
Note: With
autoImport: false, any component file that is not reachable from your entry point will be silently absent from the bundle.
Acronym Configuration
The plugin reads the acronym field from config/config.json in your project root to include your integration's identifier in the generated component names.
{
"acronym": "echo-wc"
}This produces names like:
px-int-3f8kzq2m1v-echo-wc-key-value-v1-0-0The acronym is always lowercased regardless of how it is written in the config file. If config/config.json does not exist or the acronym key is absent, it defaults to icl.
System Components
Some components have pre-assigned unique names supplied by the Polarity framework rather than generated by this plugin. These are declared in the components array in config/config.json:
{
"acronym": "echo-wc",
"components": [
{
"type": "summary",
"element": "px-int-9b69kxiww6yoxs74n4auduspb-echo-wc-summary-v5-0-0"
},
{
"type": "details",
"element": "px-int-9b69kxiww6yoxs74n4auduspb-echo-wc-details-v5-0-0"
}
]
}When the plugin discovers a file whose derived name matches a type in this list, it uses the corresponding element value as the unique name instead of generating one. Everything else — class export verification, tag rewriting, and customElements.define(...) injection — works identically.
Library Component Aliases
When integration-component-library is installed in your project, the plugin can automatically rewrite its component tags into their resolved versioned names at build time, and inject the corresponding import and customElements.define(...) calls. Register library components via the libraryComponents option:
transformComponentNames({
componentsDir: resolve(__dirname, 'src/web-components'),
libraryComponents: {
'data-grid': { className: 'DataGrid' },
},
})| Write this | Transforms to |
|---|---|
| <data-grid> | <px-lib-data-grid-v1-0-0> |
| </data-grid> | </px-lib-data-grid-v1-0-0> |
The resolved name is computed from the library's package.json version using the formula px-lib-{name}-v{version} (with dots replaced by hyphens). No library code is executed at build time.
If you prefer to handle library components yourself — for example by using staticHtml / unsafeStatic with the exported name variable, or by referencing the long-form tag name directly — set rewriteLibraryComponents: false:
transformComponentNames({
componentsDir: resolve(__dirname, 'src/web-components'),
rewriteLibraryComponents: false,
})If integration-component-library is not installed, library component rewriting is skipped automatically with a console warning.
Custom Library Components
If integration-component-library ships components, you can register them with the libraryComponents option. Each key is the short tag name (kebab-case) and the value specifies the named class export from the library:
// vite.config.ts
transformComponentNames({
componentsDir: resolve(__dirname, 'src/web-components'),
libraryComponents: {
'data-grid': { className: 'DataGrid' },
'status-badge': { className: 'StatusBadge' },
},
})These entries are used by the plugin to rewrite tags, inject imports, and register them via customElements.define(...).
| Write this | Transforms to |
|---|---|
| <data-grid> | <px-lib-data-grid-v1-0-0> |
| <status-badge> | <px-lib-status-badge-v1-0-0> |
Note:
libraryComponentsis ignored whenrewriteLibraryComponentsis set tofalse.
Component Registries
Component registries provide a generic, convention-free way for any component library to declare its tag-name mappings. The plugin reads a JSON file exported by the library and automatically handles tag rewriting, imports, and customElements.define(...) injection — without hard-coding any naming conventions.
Using a Registry
Point the componentRegistries option at one or more registry JSON files. Module specifiers are resolved via require.resolve() (found in node_modules automatically):
// vite.config.ts
transformComponentNames({
componentsDir: resolve(__dirname, 'src/web-components'),
componentRegistries: [
'integration-component-library/component-registry.json',
],
})Multiple registries are supported — for example, if you consume components from several libraries:
componentRegistries: [
'integration-component-library/component-registry.json',
'@acme/ui-library/component-registry.json',
],With a registry loaded, you can use simple tag names in your templates:
| Write this | Transforms to |
|---|---|
| <pi-button> | <pi-button-v1-0-0> |
| <pi-key-value> | <pi-key-value-v1-0-0> |
The plugin also injects the corresponding import and customElements.define(...) calls so the components are bundled and registered automatically.
Publishing a Registry
To make your component library compatible with componentRegistries, generate and export a JSON file in this format:
{
"pi-button": {
"element": "pi-button-v1-0-0",
"className": "PiButton",
"package": "@polarity/button"
},
"pi-checkbox": {
"element": "pi-checkbox-v1-0-0",
"className": "PiCheckbox",
"package": "@polarity/checkbox"
}
}Fields:
| Field | Type | Description |
|---|---|---|
| key | string | The short tag name developers write in templates (e.g. pi-button) |
| element | string | The globally unique element name to register (e.g. pi-button-v1-0-0) |
| className | string | The named class export from the package (e.g. PiButton) |
| package | string | The npm package to import the class from (e.g. @polarity/button) |
Steps to publish:
Generate the registry at build time — write a script that scans your component packages, reads each
package.jsonversion, and outputs the JSON mapping. This avoids manual maintenance.Export the file in
package.json— add an exports entry so the plugin can resolve it:{ "exports": { "./component-registry.json": "./dist/component-registry.json" } }Include it in the published package — ensure
dist/component-registry.jsonis in yourfilesarray or not excluded by.npmignore.
Plugin Options
componentsDir (required)
| Type |
|---|
| string |
The absolute path to the directory containing your web component source files. The plugin scans this directory recursively to discover components and uses it as a filter — only .ts files inside this directory are processed.
componentsDir: resolve(__dirname, 'src/web-components')autoImport
| Type | Default |
|---|---|
| boolean | true |
When true, the VIRTUAL_COMPONENTS_ID virtual entry is available and automatically imports every discovered component into the bundle. Set to false to manage your own entry point and imports.
See Opting Out of Auto-Import for details.
additionalEntry
| Type | Default |
|---|---|
| string | — |
The absolute path to a hand-written entry file for exports beyond the auto-discovered components. The build will fail immediately with a clear error if this file does not exist.
See Exporting Additional Files for a full example.
rewriteLibraryComponents
| Type | Default |
|---|---|
| boolean | true |
When true, the plugin rewrites integration-component-library component tags (registered via libraryComponents) into their resolved versioned names and injects import / customElements.define(...) calls automatically.
Set to false to handle library components yourself.
See Library Component Aliases for details.
libraryComponents
| Type | Default |
|---|---|
| Record<string, { className: string }> | — |
Additional library component definitions to register. Each key is the short tag name (kebab-case) and the value specifies the named export from integration-component-library.
User-provided entries are resolved at build time. If a key conflicts with a built-in definition, the user-provided value takes precedence and a build warning is emitted.
This option is ignored when rewriteLibraryComponents is set to false.
See Custom Library Components for a full example.
componentRegistries
| Type | Default |
|---|---|
| string[] | — |
An array of component registry module specifiers or absolute file paths. Each entry points to a JSON file that maps short tag names to their versioned element names, class names, and source packages.
Module specifiers are resolved via require.resolve() (found in node_modules automatically). Absolute paths are used as-is.
This option is ignored when rewriteLibraryComponents is set to false.
See Component Registries for format details and a full example.
Contributing
See DEVELOPMENT.md for architecture details, how to run tests, and how to contribute.
