| | |

Parcel 21 ๐Ÿ“ฆ Plugin Architecture โ€” Resolvers, transformers, bundlers & optimizers

โฑ๏ธ Estimated read: 13 min | |

Parcel’s plugin architecture is not a single extension point. It is a staged pipeline, and each stage has its own plugin type with a strictly defined contract. The build runs through seven phasesโ€”resolving, transforming, bundling, naming, packaging, optimizing, and compressingโ€”and each phase is extensible through a different plugin interface . The four plugin types that shape the bundle most directly are the resolver, the transformer, the bundler, and the optimizer. They are not interchangeable, and they are not configured in the same way. The resolver is an array in .parcelrc, the transformer is a glob map, the bundler is a single plugin name, and the optimizer is a glob map that runs over the finished bundle .

The distinction between a transformer and an optimizer is the unit of work. A transformer receives a single assetโ€”a TypeScript file, a CSS file, an SVGโ€”and returns a transformed asset. An optimizer receives an entire bundleโ€”the concatenated output of the packaging phaseโ€”and returns a transformed bundle . This is why minification is an optimizer and TypeScript compilation is a transformer. The optimizer sees the whole bundle, which is what tree-shaking and minification require. The transformer sees one file at a time, which is what compilation requires.

Key point: Resolvers turn import specifiers into absolute paths. Transformers compile individual assets and discover dependencies. Bundlers group assets into bundles. Optimizers transform the finished bundles. Each has a different configuration shape in .parcelrc, and the "..." syntax extends the default plugin chain rather than replacing it.


Why the plugin types are separated

The unit-of-work problem. A plugin that compiles TypeScript needs to see one file at a time. A plugin that minifies JavaScript needs to see the entire bundle. If both were the same plugin type, the TypeScript plugin would receive a bundle it cannot compile, and the minifier would receive a single file it cannot tree-shake. The transformer/optimizer split is the mechanism that gives each plugin the unit it needs .

The configuration problem. The four plugin types are configured differently because they match against different things. The resolver matches against the import specifier (the text in the import statement). The transformer matches against the file path. The bundler is a single plugin that receives the entire asset graph. The optimizer matches against the output bundle filename . A single configuration shape could not express all four matching rules.

The experimental status of the bundler. The bundler plugin is marked experimental in the Parcel documentation, and the documentation warns that the API is “subject to change, even between minor updates” . The resolver, transformer, and optimizer are stable. The bundler is the extension point for changing how assets are grouped, and it is the one that a typical project does not need to touch. The default bundler handles the common cases, and the custom bundler is for vendoring, module federation, or other grouping strategies that the default does not express.

The trade-off. The four-type separation adds configuration surface. A project that needs to compile a new file format uses a transformer; a project that needs to optimize the output uses an optimizer; a project that needs a custom import syntax uses a resolver. The plugin type is determined by the phase the plugin runs in, not by what the plugin does. The separation is the cost of the pipeline model.


a. Resolvers: turning specifiers into paths

A resolver plugin receives a dependencyโ€”the asset that contains the import and the specifier being importedโ€”and returns an absolute file path or null to pass to the next resolver. Resolvers are configured as an array in .parcelrc, and they run in order until one returns a result .

// .parcelrc โ€” a custom resolver before the default
{
  "extends": "@parcel/config-default",
  "resolvers": ["@company/parcel-resolver", "..."]
}

The "..." in the array is the default resolver. If the custom resolver returns null for a specifier it does not understand, the default resolver handles it. If the "..." is omitted, the custom resolver must handle every dependency in the project, or the build fails . The resolver is the plugin type for a custom import syntax, a virtual module, or a different module resolution algorithm.

// A minimal resolver plugin
import { Resolver } from '@parcel/plugin';

export default new Resolver({
  async resolve({ dependency }) {
    // dependency.specifier is the text in the import statement.
    // dependency.resolveFrom is the file that contains the import.
    if (dependency.specifier.startsWith('^/')) {
      const path = dependency.specifier.replace('^/', '/absolute/');
      return { filePath: path };
    }
    // Return null to pass to the next resolver in the chain.
    return null;
  },
});

The resolver’s return value is a filePath, which is the absolute path to the asset. The resolver does not read the file; it only determines where the file is. The transformer reads the file and compiles it .


b. Transformers: compiling assets and discovering dependencies

A transformer plugin receives a single asset, compiles it, and returns the transformed asset along with any dependencies it discovered. Transformers are configured as a glob map in .parcelrc, and multiple transformers can run in series over the same asset .

// .parcelrc โ€” a TypeScript transformer
{
  "extends": "@parcel/config-default",
  "transformers": {
    "*.{ts,tsx}": ["@parcel/transformer-typescript-tsc"],
    "icons/*.svg": ["@company/parcel-transformer-svg-icons", "..."]
  }
}

The glob map is ordered. The icons/*.svg entry is matched before the *.svg entry, so the icon-specific transformer runs first, and the "..." runs the default SVG transformer afterward. If the "..." is omitted, the default transformer is replaced, and the SVG may not be processed correctly .

// A minimal transformer plugin
import { Transformer } from '@parcel/plugin';

export default new Transformer({
  async transform({ asset }) {
    // asset.getCode() returns the source code.
    const code = await asset.getCode();

    // asset.setCode() sets the transformed code.
    asset.setCode(code.replace(/__VERSION__/g, '1.0.0'));

    // asset.addDependency() adds a dependency that Parcel will resolve.
    asset.addDependency({
      specifier: './injected.js',
      specifierType: 'esm',
    });

    return [asset];
  },
});

The transformer’s job is not just to transform the code. It is also to discover the dependencies. A TypeScript transformer discovers the imports in the TypeScript file and adds them as dependencies, so the resolver can resolve them and the transformer can process them . The asset graph is built from the dependencies that the transformers discover.


c. Bundlers and optimizers

The bundler plugin receives the entire asset graph and modifies it to add bundle nodes that group assets into output bundles. It is configured as a single plugin name, not a glob map, and it is experimental .

// .parcelrc โ€” a custom bundler
{
  "extends": "@parcel/config-default",
  "bundler": "@company/parcel-bundler"
}

The bundler is the plugin type for changing the grouping strategyโ€”splitting app and vendor code, or creating a bundle for a specific deployment target. The default bundler handles the common cases, and the custom bundler is for the cases the default does not express.

The optimizer plugin receives a bundle and its source map, and returns the transformed contents and map. Optimizers are configured as a glob map, and multiple optimizers can run in series .

// .parcelrc โ€” a custom optimizer
{
  "extends": "@parcel/config-default",
  "optimizers": {
    "*.js": ["@company/parcel-optimizer", "@parcel/optimizer-swc"]
  }
}

The optimizer is the plugin type for minification, tree-shaking, and any transformation that needs the whole bundle. The @parcel/optimizer-swc is the default minifier in recent Parcel versions, replacing Terser . A custom optimizer that runs before the default can prepend a license header or transform the bundle in a way that the minifier then processes.


Complete Example Session

// ============================================
// PART 1: THE RESOLVER
// ============================================

// .parcelrc
{
  "extends": "@parcel/config-default",
  "resolvers": ["@company/parcel-resolver", "..."]
}

// The resolver handles the "^/" specifier.
// The "..." is the default resolver for everything else.

// ============================================
// PART 2: THE TRANSFORMER
// ============================================

// .parcelrc
{
  "extends": "@parcel/config-default",
  "transformers": {
    "*.{ts,tsx}": ["@parcel/transformer-typescript-tsc"],
    "icons/*.svg": ["@company/parcel-transformer-svg-icons", "..."]
  }
}

// The TypeScript transformer compiles .ts and .tsx files.
// The icon transformer runs before the default SVG transformer.

// ============================================
// PART 3: THE BUNDLER
// ============================================

// .parcelrc
{
  "extends": "@parcel/config-default",
  "bundler": "@company/parcel-bundler"
}

// The custom bundler receives the asset graph.
// It groups the assets into bundles.
// The bundler is experimental.

// ============================================
// PART 4: THE OPTIMIZER
// ============================================

// .parcelrc
{
  "extends": "@parcel/config-default",
  "optimizers": {
    "*.js": ["@company/parcel-optimizer", "@parcel/optimizer-swc"]
  }
}

// The custom optimizer runs first.
// The SWC minifier runs second.
// The result of the first optimizer is passed to the second.

// ============================================
// PART 5: THE PLUGIN NAMING CONVENTION
// ============================================

// Official:   @parcel/{type}-{name}
// Community:  parcel-{type}-{name}
// Scoped:     @scope/parcel-{type}[-{name}]
//
// The {type} is the plugin type: resolver, transformer, bundler, optimizer.
// The {name} is descriptive of the plugin's purpose.

// ============================================
// PART 6: THE "..." SYNTAX
// ============================================

// The "..." embeds the next-priority plugin chain.
// In a resolver array: ["custom", "..."] runs custom, then default.
// In a transformer glob: ["custom", "..."] runs custom, then default.
// Without "...", the default is replaced.

// ============================================
// PART 7: THE TRANSFORMER'S DEPENDENCY DISCOVERY
// ============================================

// A transformer that adds a dependency:
import { Transformer } from '@parcel/plugin';

export default new Transformer({
  async transform({ asset }) {
    asset.addDependency({
      specifier: './injected.js',
      specifierType: 'esm',
    });
    return [asset];
  },
});

// The resolver resolves the dependency.
// The transformer processes the resolved file.

// ============================================
// PART 8: THE OPTIMIZER'S CONDITIONAL RUN
// ============================================

// An optimizer that only runs in production:
import { Optimizer } from '@parcel/plugin';

export default new Optimizer({
  async optimize({ contents, map, bundle }) {
    if (!bundle.env.shouldOptimize) {
      return { contents, map };
    }
    return { contents: minify(contents), map };
  },
});

// The optimizer checks the environment before optimizing.

// ============================================
// PART 9: THE PLUGIN CONTRACT
// ============================================

// All Parcel plugins are stateless and pure.
// The same input must produce the same output.
// No side effects, no implicit dependencies.
// The cache depends on the purity.

// ============================================
// PART 10: THE VERIFICATION CHECKLIST
// ============================================

// [ ] The resolver is in the resolvers array, with "..." if it is not universal.
// [ ] The transformer is in the transformers glob map, with "..." if it extends the default.
// [ ] The bundler is a single plugin name.
// [ ] The optimizer is in the optimizers glob map.
// [ ] The plugin names follow the naming convention.
// [ ] The plugins are stateless and pure.
// [ ] The "..." syntax is used where the default behavior should be preserved.

The ten parts covered the resolver, the transformer, the bundler, the optimizer, the naming convention, the "..." syntax, the dependency discovery, the conditional optimization, the plugin contract, and the verification. The through-line is that each plugin type has a different unit of work and a different configuration shape.


Quick Reference

The four plugin types

PluginUnit of workConfigurationStable?
ResolverA dependency specifierArray in .parcelrcYes
TransformerA single assetGlob map in .parcelrcYes
BundlerThe asset graphSingle name in .parcelrcExperimental
OptimizerA finished bundleGlob map in .parcelrcYes

The naming convention

TypeOfficialCommunityScoped
Resolver@parcel/resolver-{name}parcel-resolver-{name}@scope/parcel-resolver[-{name}]
Transformer@parcel/transformer-{name}parcel-transformer-{name}@scope/parcel-transformer[-{name}]
Bundler@parcel/bundler-{name}parcel-bundler-{name}@scope/parcel-bundler[-{name}]
Optimizer@parcel/optimizer-{name}parcel-optimizer-{name}@scope/parcel-optimizer[-{name}]

The "..." syntax

LocationWith "..."Without "..."
ResolversCustom runs, default handles the restCustom must handle everything
TransformersCustom runs, then defaultCustom replaces default
OptimizersCustom runs, then defaultCustom replaces default

Best Practices

โœ… Do This:

// Use "..." to extend the default resolver chain
"resolvers": ["@company/parcel-resolver", "..."]                   // โœ…

// Use "..." to extend the default transformer
"*.{ts,tsx}": ["@parcel/transformer-typescript-tsc"]               // โœ…

// Check shouldOptimize in an optimizer
if (!bundle.env.shouldOptimize) return { contents, map };          // โœ…

// Follow the naming convention
"@company/parcel-transformer-svg-icons"                            // โœ…

โŒ Don’t Do This:

// Omit "..." from a resolver that does not handle everything
"resolvers": ["@company/parcel-resolver"] // build may fail         // โŒ

// Use a local file path in a published config package
"./local-transformer.js" // rejected in config packages             // โŒ

// Make an optimizer stateful
// The plugin may run in multiple workers.                          // โŒ

Common Pitfalls

PitfallWhy It HappensFix
The build fails on an unresolved importThe custom resolver omitted "..."Add "..." to the array
The default transformer is not runningThe "..." is missing from the globAdd "..." to the pipeline
The plugin is not foundThe name does not follow the conventionUse the correct prefix
The optimizer runs in developmentshouldOptimize not checkedAdd the check
The cache is staleThe plugin has side effectsMake the plugin pure

Real-World Examples

1. Custom resolver

"resolvers": ["@company/parcel-resolver", "..."]

2. TypeScript transformer

"*.{ts,tsx}": ["@parcel/transformer-typescript-tsc"]

3. SVG transformer with default

"icons/*.svg": ["@company/parcel-transformer-svg-icons", "..."]

4. Custom bundler

"bundler": "@company/parcel-bundler"

5. Custom optimizer

"optimizers": { "*.js": ["@company/parcel-optimizer", "@parcel/optimizer-swc"] }

6. Optimizer conditional

if (!bundle.env.shouldOptimize) return { contents, map };

7. Dependency discovery

asset.addDependency({ specifier: './injected.js', specifierType: 'esm' });

8. Resolver returning null

return null; // pass to the next resolver

9. Naming

@company/parcel-transformer-svg-icons

10. Purity

// No state, no side effects, same input โ†’ same output

Visual

The Parcel Phases and Plugins

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  PHASES AND PLUGIN TYPES                                      โ”‚
โ”‚                                                              โ”‚
โ”‚  Resolving โ”€โ”€โ–ถ Resolver plugin                              โ”‚
โ”‚         โ”‚                                                    โ”‚
โ”‚         โ–ผ                                                    โ”‚
โ”‚  Transforming โ”€โ”€โ–ถ Transformer plugin                        โ”‚
โ”‚         โ”‚                                                    โ”‚
โ”‚         โ–ผ                                                    โ”‚
โ”‚  Bundling โ”€โ”€โ–ถ Bundler plugin                                โ”‚
โ”‚         โ”‚                                                    โ”‚
โ”‚         โ–ผ                                                    โ”‚
โ”‚  Naming โ”€โ”€โ–ถ Namer plugin                                    โ”‚
โ”‚         โ”‚                                                    โ”‚
โ”‚         โ–ผ                                                    โ”‚
โ”‚  Packaging โ”€โ”€โ–ถ Packager plugin                              โ”‚
โ”‚         โ”‚                                                    โ”‚
โ”‚         โ–ผ                                                    โ”‚
โ”‚  Optimizing โ”€โ”€โ–ถ Optimizer plugin                            โ”‚
โ”‚         โ”‚                                                    โ”‚
โ”‚         โ–ผ                                                    โ”‚
โ”‚  Compressing โ”€โ”€โ–ถ Compressor plugin                          โ”‚
โ”‚                                                              โ”‚
โ”‚  Each phase has its own plugin type and contract.            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Transformer vs Optimizer

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  THE UNIT OF WORK                                            โ”‚
โ”‚                                                              โ”‚
โ”‚  Transformer                    Optimizer                    โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”       โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”‚
โ”‚  โ”‚ One asset           โ”‚       โ”‚ One bundle          โ”‚     โ”‚
โ”‚  โ”‚ (TypeScript file)   โ”‚       โ”‚ (concatenated JS)   โ”‚     โ”‚
โ”‚  โ”‚                     โ”‚       โ”‚                     โ”‚     โ”‚
โ”‚  โ”‚ Compiles, discovers โ”‚       โ”‚ Minifies, tree-     โ”‚     โ”‚
โ”‚  โ”‚ dependencies        โ”‚       โ”‚ shakes, prepends    โ”‚     โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ”‚
โ”‚                                                              โ”‚
โ”‚  The transformer sees the file.                             โ”‚
โ”‚  The optimizer sees the bundle.                             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The "..." Syntax

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  EXTENDING VS REPLACING                                      โ”‚
โ”‚                                                              โ”‚
โ”‚  With "..."                    Without "..."                 โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”       โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”‚
โ”‚  โ”‚ Custom plugin runs  โ”‚       โ”‚ Custom plugin runs  โ”‚     โ”‚
โ”‚  โ”‚ then default runs   โ”‚       โ”‚ default is gone     โ”‚     โ”‚
โ”‚  โ”‚                     โ”‚       โ”‚                     โ”‚     โ”‚
โ”‚  โ”‚ Safe extension      โ”‚       โ”‚ Full replacement    โ”‚     โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ”‚
โ”‚                                                              โ”‚
โ”‚  Use "..." when the default behavior should be preserved.   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
ResolverTurns specifiers into paths; array in .parcelrc
TransformerCompiles assets; glob map in .parcelrc
BundlerGroups assets; single name; experimental
OptimizerTransforms bundles; glob map in .parcelrc
The "..." syntaxExtends the default chain; omitting it replaces
Plugin purityStateless, pure, no side effects
Naming convention@parcel/{type}-{name} or parcel-{type}-{name}

Key takeaways:

  • The plugin type is determined by the phase, not by the task. A resolver runs in the resolving phase, a transformer in the transforming phase, a bundler in the bundling phase, and an optimizer in the optimizing phase. The phase determines the unit of work and the configuration shape .
  • Resolvers and transformers are configured differently. The resolver is an array that runs in order until one returns a result. The transformer is a glob map that matches the file path. The "..." syntax extends the default chain in both cases, but the shape is different .
  • The bundler is experimental. The documentation warns that the bundler API may change between minor versions. The default bundler handles the common cases, and the custom bundler is for the cases it does not express .
  • The optimizer receives the whole bundle. This is what distinguishes it from the transformer, which receives a single asset. Minification and tree-shaking are optimizers because they need the whole bundle. Compilation is a transformer because it needs one file at a time .
  • Plugins must be pure and stateless. The plugin may run in multiple workers that cannot communicate. The cache depends on the plugin producing the same output for the same input with no side effects .

Remember: Parcel’s plugin architecture is a staged pipeline, and each stage has its own plugin type. The resolver turns specifiers into paths. The transformer compiles assets and discovers dependencies. The bundler groups assets into bundles. The optimizer transforms the finished bundles. The four types are not interchangeable, and they are not configured the same way. The "..." syntax is the extension mechanism: it preserves the default behavior while inserting the custom plugin. The plugin is pure and stateless, and the cache depends on it. The bundler is experimental, and the resolver, transformer, and optimizer are stable.

Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!