β οΈ Status: In Active Development > This project is currently in its early stages and is undergoing rapid iteration to ensure library-grade robustness. While the core codemod architecture is mathematically sound and tested, please be aware that breaking changes may occur. To see our immediate priorities, known limitations, and upcoming milestones, please review the Roadmap inCONTRIBUTION.md.
Welcome to the Whitelabel Extractor! This is a lightning-fast, Rust-based AST transformation tool built on top of SWC.
In multi-tenant applications, codebases often become cluttered with conditional logic (if (brand === 'X')) to render different configurations, strings, or UI components for different targets. wl-extractor solves this by allowing developers to write standard, localized variables. The tool automatically extracts those variables, generates a strictly typed registry, and safely rewrites your codebase to use them dynamically.
It runs an SWC-powered AST (Abstract Syntax Tree) traversal over your codebase to do three things:
- Extracts: Finds any variable or function exported with a
// whitelabelcomment. - Generates: Builds a central TypeScript registry (
brandA.generated.tsx,index.ts, etc.) based on those exports. - Rewrites (Codemod): Goes through your files (like Next.js
page.tsxfiles) and surgically replaces direct imports of those components/variables with property accesses on the generatedwhitelabelobject.
It resolves paths using Node.js rules and respects your tsconfig.json path aliases (e.g., @/components/*).
Before running the tool, create a whitelabel.config.json file in the root of your project:
{
"src": "src/",
"patterns": ["**/*.tsx", "**/*.ts"],
"output_dir": "whitelabel",
"default_target": "def",
"tsconfig": "tsconfig.json"
}To mark a variable, function, or component for extraction, simply add a magic comment directly above its export statement.
The directive parser uses a formal Context-Free Grammar (CFG), allowing for a highly flexible, natural-language syntax:
WHITELABEL [ ":" ] [ modifier [, modifier ...] ]
modifier:
"*"
| FOR [ "=" | ":" ] value
| KEY ( "=" | ":" ) value
| AS [ "=" | ":" ] value
| OPTIONAL
value:
string
| "'" string "'"
| '"' string '"'
Available Modifiers (Case-Insensitive):
*(Wildcard): Applies the exported symbol to all known targets in your project. Cannot be combined withfororoptional.for: (Optional) The specific brand/tenant this code applies to. You can use=,:, or omit the operator entirely (e.g.,for=variant1,for:'variant1', orfor variant1). If omitted, it falls back to thedefault_targetdefined in your config.key: (Optional) A custom key for the registry. Requires=or:(e.g.,key=BG_COLOR). If omitted, the tool uses the variable's original exported name.as: (Optional) An ergonomic alias forkeythat does not require an operator (e.g.,as BG_COLOR).optional: Marks the key as explicitly optional. The registry will safely injectundefinedfor any targets that do not implement this key, forcing TypeScript compilation errors over runtime crashes. Cannot be combined with*.
Note: The : immediately following whitelabel is entirely optional. Modifier values can be unquoted, single-quoted ('...'), or double-quoted ("...").
Examples: basic-usages fixture input
/**
* 1. The most formal `whitelabel` marker
*/
// whitelabel: for=variant1, key=BG_COLOR
export const variant1_bgClassname: string = "bg-red-100";
/**
* 2. If `for` is omitted, it defaults to your config's `default_target`
*/
// whitelabel
export const BG_COLOR: string = "bg-red-200";
/**
* 3. The most natural `whitelabel` marker.
* `as` works identically to `key`, but allows you to omit the operator.
*/
// whitelabel for 'variant2' as 'BG_COLOR'
export const variant2_bgClassname: string = "bg-red-300";
/**
* 4. Operators and quotes are entirely optional.
*/
// whitelabel for variant3 as BG_COLOR
export const variant3_bgClassname: string = "bg-red-400";
/**
* 5. Wildcard marker
* Applies this variable to all targets globally.
*/
// whitelabel *
export const DEFAULT_HEADER = "Header";
/**
* 6. Optional marker
* If other targets don't implement this, they safely receive `undefined`.
*/
// whitelabel optional, for=variant1
export const VARIANT_ONLY_FEATURE = true;Execute the CLI tool in your terminal:
cargo install --path . ; wl-extractor(Tip: For CI/CD or piping to Prettier, use wl-extractor --file-name-only to suppress human-readable logs.)
The tool will automatically generate a registry in your output_dir containing:
brandA.generated.tsxandbrandB.generated.tsx- A customizable
index.tsstarter template. Unlike the generated files, this file is yours to edit! It unites the targets into a strictly typedWhitelabelConfigand provides a template for you to wire up your own runtime/SSR brand resolution logic.
Most importantly, it rewrites your local usages. If you imported BrandAHeader somewhere else in your code, the tool transforms it:
Before: basic-usages/app/home/page.tsx
import { Heading } from "./_components/heading";
// whitelabel: key=BG_COLOR
export const bgClassname = "bg-red-500";
// whitelabel: for=variant1, key=BG_COLOR
export const variant1_bgClassname = "bg-green-500";
const Homepage = () => (
<div className={`h-full w-full ${bgClassname}`}>
<Heading />
</div>
);
export default Homepage;After: test snapshot
import whitelabel from "../whitelabel"; // Injected automatically!
import { Heading } from "./_components/heading";
// whitelabel: key=BG_COLOR
export const bgClassname = "bg-red-500";
// whitelabel: for=variant1, key=BG_COLOR
export const variant1_bgClassname = "bg-green-500";
const Homepage = () => (
// Surgically rewritten to use the global registry
<div className={`h-full w-full ${whitelabel.BG_COLOR}`}>
<whitelabel.Heading />
</div>
);
export default Homepage;This tool works like magic, but even magic needs a strict set of rules.
When dealing with automated AST (Abstract Syntax Tree) transformations, trying to support every single edge case of the TypeScript specification is a one-way ticket to brittle, unpredictable builds. v1 explicitly ignores pure types and complex declarations.
If you attempt to attach a // whitelabel directive to the following syntax, the CLI will throw a validation error and skip the extraction safely:
- β Types & Interfaces: (
export type Config = {},export interface Props {}) - β Enums: (
export enum Colors {}) - β Classes: (
export class ApiClient {}) - β Namespaces/Modules: (
export module Utils {}) - β Named Re-exports: (
export { foo as companyName })
(π‘ Note: You can still use classes, enums, and complex types extensively throughout your codebase! You just cannot tag them as the root whitelabel target to be extracted.)
The extractor operates in three distinct phases using swc_core for robust Abstract Syntax Tree (AST) manipulation.
- File Scanning: The tool globs all files matching the
patternsin your config. - Lexical Analysis: It parses each file into an AST and extracts all
SingleThreadedComments. - LALRPOP Parsing: When it encounters an
ExportDecl(likeexport constorexport function), it uses a mathematically sound Context-Free Grammar (powered by LALRPOP) to safely parse leading comments for directives. - Data Extraction: It extracts the physical file path, the exported symbol name, and maps it to the requested
targetandkey.
- The collected entries are fed into a central
WhitelabelRegistry. This registry acts as the source of truth, validating duplicate keys, fanning out wildcard (*) configurations, and injectingundefinedfallbacks foroptionalkeys. - It writes
[target].generated.tsxfiles, importing the original symbols from their physical paths and structuring them into a single object. - It generates an
index.tsfile that imports all target files and exports a unified TypeScript union and config record.
This is the most complex phase. The tool must safely replace local variables with the global whitelabel object.
- Path Resolution: The
SymbolScannerreads yourtsconfig.jsonpathsmapping. It interceptsImportDeclnodes and resolves raw import strings (e.g.,@/components/Hero) into absolute physical paths on the disk. - Tracking References: It checks if the imported symbol matches a known whitelabel entry originating from that exact physical file. If it matches, it flags the AST identifier's internal
Id. - AST Transformation: The
WhitelabelRewriterwalks the AST and replaces the flagged identifiers:- Standard Identifiers:
foobecomeswhitelabel.foo. - Object Shorthands:
{ foo }becomes{ foo: whitelabel.foo }. - JSX Elements:
<Foo />becomes<whitelabel.Foo />.
- Standard Identifiers:
- Import Injection: If a file was modified, the tool calculates a dynamic, safe relative path from the current file to the generated registry using
pathdiff. It then injectsimport whitelabel from "..."at the top of the AST. - Rename Detection: If you change a
keyin your magic comment, the tool parses the old generated registry, detects the diff, and runs a secondary codemod to rename allwhitelabel.oldKeytowhitelabel.newKeyacross the entire codebase. - Transactional Codemods (
TxFS): To guarantee project safety, all file modifications are staged in an isolated, in-memory buffer. A single atomic.commit()is executed at the very end of the pipeline. If any file write fails, the entire codemod rolls back, ensuring your repository is never left in a partially migrated state.