This guide is the deep, source-based reference for operating, self-hosting, configuring, and extending IT-Tools — the open-source collection of ~100 client-side developer utilities by Corentin Thomasset. It is written for administrators deploying IT-Tools on their own infrastructure, and for developers who want to understand the architecture, add tools, or contribute.
If you are an end user looking for how to use the tools, see the User Guide.
Table of Contents
- Executive Summary
- Architecture
- Repository Structure
- Requirements
- Configuration Reference
- Deployment
- Operations
- Security Analysis
- Building from Source & Developer Workflow
- Extending: Adding a New Tool
- Internals Deep Dive
- Code Quality & Conventions
- Testing Strategy
- Potential Improvements
- Quick Reference
1. Executive Summary
IT-Tools is a Vue 3 single-page application that bundles around one hundred small, self-contained utilities behind one searchable interface. Its defining architectural property is that it is 100% client-side: there is no backend service, no database, no application server, and no API. The deployable artifact is a directory of static files (HTML, JS, CSS, assets) served by a plain web server.
Key operational facts:
| Property | Value |
|---|---|
| Type | Static single-page web application (SPA) |
| Framework | Vue 3 + TypeScript |
| UI library | Naive UI + a small custom c-* component kit, UnoCSS for atomic styling |
| Build tool | Vite 4 (via pnpm) |
| Runtime backend | None — static files only |
| Production server | nginx (in the official Docker image) |
| Default container port | 80 (commonly published as host 8080) |
| Persistence | None server-side; user state lives in the browser (local storage / URL) |
| Analytics | Plausible, disabled by default; privacy-friendly when enabled |
| Configuration | Build-time environment variables (baked into the bundle) |
| License | GNU GPLv3 |
| Analyzed version | 2024.10.22-7ca5933 (package.json), commit d505845 |
What this means for administrators: deploying IT-Tools is deploying a static site. There is nothing to scale, no stateful component to back up, and the security surface is minimal (no server-side input handling, no secrets at runtime). The most important thing to understand is that all configuration is applied at build time — changing analytics, the base path, or banners requires rebuilding the image, not setting an environment variable on the running container. This is covered in detail in §5.
2. Architecture
2.1 High-level picture
Browser (the entire runtime)
┌───────────────────────────────────────────────────────────────┐
│ Vue 3 SPA │
│ │
│ main.ts ──> createApp(App) │
│ ├─ Pinia (state: style, tools, command-palette) │
│ ├─ vue-router (routes auto-generated from tools) │
│ ├─ vue-i18n (locale messages, persisted) │
│ ├─ naive-ui (component library) │
│ ├─ plausible plugin (no-op unless enabled) │
│ └─ vue-shadow-dom │
│ │
│ App.vue ──> <layout> ──> <RouterView/> │
│ │ │
│ ├─ base.layout (sidebar + top bar + home) │
│ └─ tool.layout (per-tool header + content) │
│ │
│ Tool registry: src/tools/<name>/index.ts -> defineTool() │
│ aggregated in src/tools/index.ts -> toolsByCategory[] │
│ │
│ State persisted in: localStorage + URL query params │
│ Service Worker (PWA): offline cache, auto-update │
└───────────────────────────────────────────────────────────────┘
▲
│ static files over HTTP(S)
┌──────────────────────┴───────────────────────────────────────┐
│ nginx:stable-alpine — serves /usr/share/nginx/html │
│ SPA fallback: try_files $uri $uri/ /index.html │
└───────────────────────────────────────────────────────────────┘
There is no tier below nginx. The web server's only job is to serve the compiled assets and route all unknown paths back to index.html so the client-side router can handle them.
2.2 The tool registry — the core architectural pattern
The single most important pattern in the codebase is the tool registry. Each tool is a self-contained folder under src/tools/<tool-name>/ whose index.ts exports a tool descriptor built with the defineTool() factory:
// src/tools/uuid-generator/index.ts
import { Fingerprint } from '@vicons/tabler';
import { defineTool } from '../tool';
import { translate } from '@/plugins/i18n.plugin';
export const tool = defineTool({
name: translate('tools.uuid-generator.title'),
path: '/uuid-generator',
description: translate('tools.uuid-generator.description'),
keywords: ['uuid', 'v4', 'random', 'id', /* ... */],
component: () => import('./uuid-generator.vue'), // lazy-loaded
icon: Fingerprint,
redirectFrom: ['/old-path'], // optional legacy redirects
createdAt: new Date('2024-01-31'), // optional; drives the "New" badge
});
The defineTool factory (src/tools/tool.ts) is deliberately tiny — it only computes the isNew flag:
export function defineTool(tool: WithOptional<Tool, 'isNew'>) {
const isNew = tool.createdAt ? isAfter(tool.createdAt, subWeeks(new Date(), 2)) : false;
return { isNew, ...tool };
}
So a tool is "new" if its createdAt is within the last two weeks.
All tools are imported and grouped into categories in src/tools/index.ts:
export const toolsByCategory: ToolCategory[] = [
{ name: 'Crypto', components: [tokenGenerator, hashText, bcrypt, /* ... */] },
{ name: 'Converter', components: [dateTimeConverter, baseConverter, /* ... */] },
// ... 10 categories total
];
export const tools = toolsByCategory.flatMap(({ components }) => components);
export const toolsWithCategory = toolsByCategory.flatMap(({ components, name }) =>
components.map(tool => ({ category: name, ...tool })),
);
Important
This registry is the backbone of the whole app. The router, the sidebar menu, the home grid, the favorites system, and the command palette are all derived from
toolsByCategory/tools. Adding a tool to the registry automatically wires it into navigation, search, routing, and favorites — there is no separate route table or menu definition to maintain.
2.3 Routing
src/router.ts builds the route table programmatically from the registry:
const toolsRoutes = tools.map(({ path, name, component, ...config }) => ({
path, name, component,
meta: { isTool: true, layout: layouts.toolLayout, name, ...config },
}));
const toolsRedirectRoutes = tools
.filter(({ redirectFrom }) => redirectFrom?.length)
.flatMap(({ path, redirectFrom }) =>
redirectFrom!.map(src => ({ path: src, redirect: path })));
const router = createRouter({
history: createWebHistory(config.app.baseUrl),
routes: [
{ path: '/', name: 'home', component: HomePage },
{ path: '/about', name: 'about', component: () => import('./pages/About.vue') },
...toolsRoutes,
...toolsRedirectRoutes,
...(config.app.env === 'development' ? demoRoutes : []), // UI lab, dev only
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
],
});
Notes:
- History mode is
createWebHistorywith a configurable base URL (config.app.baseUrl). This is why nginx needs the SPA fallback — deep links like/jwt-parsermust resolve toindex.html. - Redirects are first-class: a tool can declare
redirectFrom: ['/old-name']to preserve old bookmarks (e.g./cypher→/encryption,/hash→/hash-text). - A dev-only "UI lab" (
/c-lib, thedemoRoutes) is mounted only whenconfig.app.env === 'development'.
2.4 State management (Pinia)
There are three Pinia stores:
| Store | File | Responsibility | Persistence |
|---|---|---|---|
style |
src/stores/style.store.ts |
Dark theme, small-screen detection, menu collapse | useDark() + useStorage('isMenuCollapsed') |
tools |
src/tools/tools.store.ts |
Favorites, i18n-resolved tool list, categories, new tools | useStorage('favoriteToolsName') |
command-palette |
src/modules/command-palette/command-palette.store.ts |
Fuzzy search over tools + quick actions | none |
The tools store re-projects the static registry through i18n (resolving titles/descriptions for the active locale), groups by category, and exposes favorite management:
favoriteToolsName = useStorage('favoriteToolsName', []) // string[] of tool paths
addToolToFavorites / removeToolFromFavorites / isToolFavorite / updateFavoriteTools
newTools = tools.filter(({ isNew }) => isNew)
The command-palette store builds a searchable list of all tools plus actions (Random tool, Toggle dark mode, GitHub repository, Report a bug, About) and runs a Fuse.js fuzzy search weighted toward the tool name:
useFuzzySearch({
search: searchPrompt,
data: searchOptions,
options: { keys: [{ name: 'name', weight: 2 }, 'description', 'keywords', 'category'], threshold: 0.3 },
});
2.5 Layouts
| Layout | File | Used for |
|---|---|---|
base.layout |
src/layouts/base.layout.vue |
The shell: sidebar (logo, category menu, favorites, footer with version + commit), top bar (menu/home/search/locale/nav buttons/"Buy me a coffee") |
tool.layout |
src/layouts/tool.layout.vue |
Wraps a tool page with its title, description, favorite button, and sets <head> meta (title/description/keywords) via @vueuse/head |
The active layout is selected per-route via route.meta.layout; the home and About pages use the base layout, tools use the tool layout.
2.6 Internationalization
vue-i18n(composition mode,legacy: false) with messages compiled fromlocales/*.ymland per-toolsrc/tools/*/localesat build time by@intlify/unplugin-vue-i18n.- A
translate()helper (src/plugins/i18n.plugin.ts) is used inside tool descriptors; it falls back to the key if a translation is missing. - The active locale is persisted to local storage and synced to the i18n runtime in
App.vue.
2.7 Build-time plugins (Vite)
The Vite config (vite.config.ts) wires a notable plugin chain that shapes the developer ergonomics:
unplugin-auto-import— auto-imports Vue, vue-router,@vueuse/core, vue-i18n, and selected Naive UI composables, so most files don'timport { ref, computed }.unplugin-vue-components— auto-registers components fromsrc/, the Naive UI resolver, and icon components (<icon-mdi:...>,<icon-...>).unplugin-icons— on-demand icon components.vite-plugin-vue-markdown— lets.mdfiles be used as components (used by the Git/Regex cheatsheets).vite-svg-loader— import SVGs as Vue components (?component).vite-plugin-pwa(generateSW,registerType: 'autoUpdate') — generates the service worker and web app manifest (name "IT Tools", theme color#18a058).- UnoCSS — atomic CSS used throughout templates (
flex,mt-12px,op-60, …).
Note
Because of auto-import and auto-component registration, source files use
ref,computed,useI18n,<c-button>,<icon-mdi:...>, etc. without explicit imports. This is intentional and is the project's house style — keep it in mind when reading the code.
3. Repository Structure
it-tools/
├─ src/
│ ├─ main.ts # App bootstrap: createApp + plugins
│ ├─ App.vue # Root component, theme + layout selection
│ ├─ router.ts # Routes generated from the tool registry
│ ├─ config.ts # figue config schema (build-time env)
│ ├─ themes.ts # Naive UI light/dark theme overrides
│ ├─ shims.d.ts # Module type declarations (.vue, .md, libs)
│ ├─ tools/
│ │ ├─ index.ts # toolsByCategory / tools / toolsWithCategory
│ │ ├─ tool.ts # defineTool() factory
│ │ ├─ tools.types.ts # Tool / ToolCategory types
│ │ ├─ tools.store.ts # Pinia store (favorites, i18n, categories)
│ │ └─ <tool-name>/ # one folder per tool (~100)
│ │ ├─ index.ts # defineTool descriptor
│ │ ├─ <tool>.vue # the tool UI
│ │ ├─ <tool>.service.ts # pure logic (optional)
│ │ ├─ *.test.ts # unit tests (optional)
│ │ └─ locales/ # per-tool translations (optional)
│ ├─ components/ # Shared components (ToolCard, MenuLayout, …)
│ ├─ ui/ # Custom "c-*" UI kit (c-button, c-card, …) + /c-lib demo
│ ├─ composable/ # Reusable composables (copy, fuzzySearch, validation, …)
│ ├─ modules/ # Feature modules (command-palette, i18n, tracker, shared)
│ ├─ stores/ # Global Pinia stores (style)
│ ├─ layouts/ # base.layout, tool.layout
│ ├─ pages/ # Home, About, 404
│ ├─ plugins/ # i18n, naive, plausible Vue plugins
│ ├─ utils/ # Generic helpers
│ └─ assets/ # Images, gradients
├─ locales/ # App-level i18n (en, fr, de, es, pt, uk, vi, zh, no)
├─ scripts/ # create-tool.mjs, build-locales-files.mjs, release.mjs, …
├─ _templates/ # hygen templates (UI component scaffolding)
├─ Dockerfile # Multi-stage build → nginx
├─ nginx.conf # SPA server config
├─ vite.config.ts # Build + plugin configuration
└─ package.json # Scripts and dependencies
Component layering
src/ui/— a small in-house component kit prefixedc-(c-button,c-card,c-input-text,c-tooltip,c-modal,c-select,c-table, …). These wrap or replace Naive UI primitives for a consistent look. The/c-libroute (development only) is a living style guide for them.src/components/— app-specific shared components:ToolCard,ColoredCard,CollapsibleToolMenu,MenuLayout,NavbarButtons,FavoriteButton,MenuIconItem, and copyable input/area helpers (InputCopyable,TextareaCopyable,SpanCopyable,FormatTransformer).src/composable/— logic reuse:useCopy(clipboard + toast),useFuzzySearch(Fuse.js),useValidation(rule-based input validation),useQueryParam/useQueryParamOrStorage(state in URL + storage),useDownloadFileFromBase64,computedRefreshable,useDebouncedRef.
4. Requirements
Runtime (to serve the app)
| Requirement | Detail |
|---|---|
| Docker | Recommended path. Any host that runs Docker. |
| Web server (manual) | Any static-file server (nginx, Caddy, Apache, S3+CDN) capable of SPA fallback. |
| Memory | Minimal — the production container is nginx serving static files; tens of MB. |
| Disk | Tens of MB for the image/bundle. |
| TLS | Not provided by the app; terminate TLS at a reverse proxy or CDN. |
Build (to compile from source)
| Requirement | Detail |
|---|---|
| Node.js | LTS (the Dockerfile uses node:lts-alpine; @tsconfig/node18 targets Node 18+). |
| pnpm | 9.11.0 (declared via packageManager). |
| Build memory | The build raises Node's heap: NODE_OPTIONS=--max_old_space_size=4096. Give the build host ≥4 GB. |
Warning
The production build is memory-hungry — it sets
--max_old_space_size=4096. Building on a small VPS (1–2 GB RAM) can OOM-kill the build. Build on a 4 GB+ machine or use the prebuilt image.
5. Configuration Reference
This is the most important section for self-hosters. IT-Tools is configured at build time, not run time.
5.1 The configuration schema
src/config.ts defines the entire configuration surface using figue. Each key declares a type, a default, and the environment variable it reads:
| Config key | Env variable | Type | Default | Purpose |
|---|---|---|---|---|
app.version |
PACKAGE_VERSION |
string | 0.0.0 |
App version shown in the sidebar footer. |
app.lastCommitSha |
VITE_VERCEL_GIT_COMMIT_SHA |
string | '' |
Commit SHA shown in the footer (links to GitHub). |
app.baseUrl |
BASE_URL |
string | / |
Base path the app is served from. |
app.env |
VITE_VERCEL_ENV |
enum | development |
One of production, development, preview, test. Gates the dev-only UI lab. |
plausible.isTrackerEnabled |
VITE_TRACKER_ENABLED |
boolean | false |
Master switch for Plausible analytics. |
plausible.domain |
VITE_PLAUSIBLE_DOMAIN |
string | '' |
Plausible site domain. |
plausible.apiHost |
VITE_PLAUSIBLE_API_HOST |
string | '' |
Plausible API host (for self-hosted Plausible). |
plausible.trackLocalhost |
(none) | boolean | false |
Whether to track localhost. |
showBanner |
VITE_SHOW_BANNER |
boolean | false |
Show the "star us on GitHub" banner on home. |
showSponsorBanner |
VITE_SHOW_SPONSOR_BANNER |
boolean | false |
Show the sponsor banner. |
The config is loaded from import.meta.env, validated, and frozen at startup:
export const config = figue({ /* schema above */ })
.loadEnv({ ...import.meta.env, PACKAGE_VERSION: import.meta.env.PACKAGE_VERSION })
.validate()
.getConfig();
5.2 Why these are build-time, not run-time
Vite statically replaces import.meta.env.* references during vite build. The values are compiled into the JavaScript bundle. PACKAGE_VERSION is wired explicitly in vite.config.ts:
define: {
'import.meta.env.PACKAGE_VERSION': JSON.stringify(process.env.npm_package_version),
},
Caution
Setting these variables on the running container (e.g.
docker run -e VITE_TRACKER_ENABLED=true …) has no effect — the bundle was already built. To change analytics, the base path, or banners, you must rebuild the image with the variables present at build time, or build the app yourself and serve the resultingdist/.
5.3 Common build-time configurations
Serve under a sub-path (e.g. behind a reverse proxy at /tools/):
# Build with a base path
BASE_URL=/tools/ pnpm build
# then serve dist/ at https://example.com/tools/
Enable privacy-friendly analytics (build a custom image):
# In a custom Dockerfile derived from the project, set before `pnpm build`:
ENV VITE_TRACKER_ENABLED=true
ENV VITE_PLAUSIBLE_DOMAIN=tools.example.com
ENV VITE_PLAUSIBLE_API_HOST=https://plausible.example.com
The Plausible plugin (src/plugins/plausible.plugin.ts) is a no-op unless isTrackerEnabled is true — it injects a fake tracker that does nothing, so the default self-hosted build collects nothing.
5.4 nginx configuration
The bundled server config (nginx.conf) is intentionally minimal:
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html; # SPA fallback
}
}
The try_files … /index.html line is essential: it lets the client-side router handle deep links. If you serve the app with a different web server, replicate this fallback.
6. Deployment
6.1 Docker (recommended)
docker run -d \
--name it-tools \
--restart unless-stopped \
-p 8080:80 \
corentinth/it-tools:latest
| Flag | Meaning |
|---|---|
-d |
Detached (background). |
--restart unless-stopped |
Auto-restart on reboot/crash. |
-p 8080:80 |
Publish container port 80 as host 8080. |
GitHub Container Registry alternative: ghcr.io/corentinth/it-tools:latest.
6.2 Docker Compose
services:
it-tools:
image: corentinth/it-tools:latest
container_name: it-tools
restart: unless-stopped
ports:
- "8080:80"
docker compose up -d
6.3 Behind a reverse proxy with TLS
The container speaks plain HTTP on port 80. Terminate TLS upstream. Example with Traefik labels:
services:
it-tools:
image: corentinth/it-tools:latest
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.ittools.rule=Host(`tools.example.com`)"
- "traefik.http.routers.ittools.entrypoints=websecure"
- "traefik.http.routers.ittools.tls.certresolver=le"
- "traefik.http.services.ittools.loadbalancer.server.port=80"
Tip
Serving at a domain root needs no special config. Serving under a sub-path requires a build-time
BASE_URL(see §5.3) so asset URLs and the router base line up.
6.4 Homelab platforms
IT-Tools is packaged for one-click install on Cloudron, Tipi, and Unraid (Community Applications). These wrap the same image.
6.5 Static hosting (S3/CDN/Pages)
Because the output is static, you can build locally and upload dist/ to any static host:
pnpm install
pnpm build # outputs ./dist
# upload ./dist to S3 / Netlify / Cloudflare Pages / GitHub Pages
Ensure the host rewrites unknown routes to /index.html (SPA fallback). The official project is continuously deployed on Vercel.
6.6 Deployment checklist
- [ ] Choose a deployment method (Docker / Compose / static host).
- [ ] If serving under a sub-path, build with
BASE_URL=/your-path/. - [ ] Put a TLS-terminating reverse proxy or CDN in front for HTTPS.
- [ ] Confirm SPA fallback works (deep link like
/jwt-parserloads on refresh). - [ ] Confirm the camera-using tools work only over HTTPS/localhost (browser requirement).
- [ ] Decide on analytics (default: off). If enabling, rebuild with Plausible env vars.
- [ ] Set
--restart unless-stopped(or your orchestrator's equivalent).
7. Operations
Because there is no backend or database, operations are simple.
Start / stop / status
docker start it-tools
docker stop it-tools
docker ps --filter name=it-tools
$ docker ps --filter name=it-tools
CONTAINER ID IMAGE STATUS PORTS NAMES
a1b2c3d4e5f6 corentinth/it-tools:latest Up 2 hours 0.0.0.0:8080->80/tcp it-tools
Logs
docker logs -f it-tools # nginx access/error logs
Updating
docker pull corentinth/it-tools:latest
docker stop it-tools && docker rm it-tools
docker run -d --name it-tools --restart unless-stopped -p 8080:80 corentinth/it-tools:latest
(With Compose: docker compose pull && docker compose up -d.)
Tip
Pin a specific image tag instead of
latestin production so updates are deliberate and reproducible.
Backup
There is nothing server-side to back up — no database, no uploads, no runtime state. All user state (favorites, theme, language) lives in each user's browser. Your "backup" is simply your deployment configuration (Compose file / Dockerfile customizations) kept in version control.
Health checking
A simple HTTP check on the root path is sufficient:
curl -fsS http://localhost:8080/ > /dev/null && echo "OK"
Resource expectations
The runtime is nginx serving static files. Expect very low CPU and memory; a single small container comfortably serves a team or a department. Scaling, if ever needed, is horizontal and stateless (put N identical containers behind a load balancer or CDN).
8. Security Analysis
8.1 Threat surface is small by design
| Aspect | Assessment |
|---|---|
| Server-side input handling | None. No backend means no server-side injection, deserialization, or SSRF surface. |
| Secrets at runtime | None. The app holds no credentials; there is nothing to leak from the server. |
| Data exfiltration | Tools run in the browser; data-transformation tools transmit nothing. |
| Analytics | Off by default; when on, Plausible is cookieless and collects page views only. |
| Authentication | None (and none needed — it's a public, read-only static app). |
8.2 Client-side considerations
- Tools process untrusted input in the browser. Formatters and parsers (JSON/XML/YAML/SQL, JWT, user-agent, etc.) operate on user-supplied text. The risk is limited to the user's own session. The HTML WYSIWYG editor and Markdown to HTML render user content; the project includes
dompurifyfor sanitization where HTML is produced — preserve that when modifying those tools. - Cryptographic tools are convenience-grade. The Encrypt/decrypt tool exposes RC4 and TripleDES alongside AES. RC4 is insecure and TripleDES is legacy. Document this for users (the User Guide does) and prefer AES. Hashing is done with CryptoJS; bcrypt with
bcryptjs. - Camera recorder requires explicit browser permission and a secure context (HTTPS or localhost). Media stays client-side.
8.3 Hardening recommendations for self-hosters
- [ ] Serve over HTTPS via a reverse proxy/CDN. Several tools (camera) require a secure context, and HTTPS protects the app delivery itself.
- [ ] Add security headers at the proxy:
Content-Security-Policy,X-Content-Type-Options: nosniff,Referrer-Policy,X-Frame-Options/frame-ancestors. A strict CSP is feasible since the app loads its own bundled assets (loosen only if you enable analytics to an external Plausible host). - [ ] Keep analytics off (the default) unless you operate your own Plausible instance.
- [ ] Pin image versions and rebuild/redeploy to pick up dependency security fixes.
- [ ] Restrict network exposure if it's an internal tool (bind to the LAN/VPN, not the public internet).
Note
Verify dependency advisories periodically. The app pulls in a broad set of client libraries (e.g.
crypto-js,node-forge,monaco-editor,marked,markdown-it,dompurify); rebuilding on a current base keeps these patched. There is a Dependency Dashboard issue tracking this upstream.
9. Building from Source & Developer Workflow
Setup
pnpm install
Develop with hot reload
pnpm dev # Vite dev server, default http://localhost:5173
In development, the UI lab is available at /c-lib (the c-* component gallery).
Type-check, build, preview
pnpm build # vue-tsc --noEmit && vite build → ./dist
pnpm preview # serve the production build on :5050
pnpm typecheck # vue-tsc only
Lint
pnpm lint # eslint over .vue/.ts/.js (antfu config)
npm scripts reference
| Script | Command | Purpose |
|---|---|---|
dev |
vite |
Dev server with HMR. |
build |
vue-tsc --noEmit && NODE_OPTIONS=--max_old_space_size=4096 vite build |
Type-check + production build. |
preview |
vite preview --port 5050 |
Serve the built bundle. |
test / test:unit |
vitest --environment jsdom |
Unit tests. |
test:e2e |
playwright test |
End-to-end tests. |
test:e2e:dev |
BASE_URL=… NO_WEB_SERVER=true playwright test |
E2E against a running dev server. |
coverage |
vitest run --coverage |
Coverage report. |
typecheck |
vue-tsc --noEmit -p tsconfig.vitest.json |
Type-check only. |
lint |
eslint src --ext … |
Lint. |
script:create:tool |
node scripts/create-tool.mjs |
Scaffold a new tool. |
script:create:ui |
hygen generator ui-component |
Scaffold a UI component. |
release |
node ./scripts/release.mjs |
Release automation. |
10. Extending: Adding a New Tool
Adding a tool is the most common contribution. The repo ships a scaffolder.
10.1 Scaffold
pnpm run script:create:tool my-tool-name
scripts/create-tool.mjs creates src/tools/my-tool-name/ containing:
my-tool-name.vue— the UI (starts with aLorem ipsumplaceholder).index.ts— adefineTool({...})descriptor pre-filled with the title (Title Case),path: '/my-tool-name', keywords from the name, a default icon, and today'screatedAt.my-tool-name.service.ts— empty; put pure logic here.my-tool-name.service.test.ts— a Vitest stub.my-tool-name.e2e.spec.ts— a Playwright stub (checks the page titleMy-tool-name - IT Tools).
It also inserts the import into src/tools/index.ts automatically (near the top of the import list).
10.2 Finish wiring it
The scaffolder adds the import but does not place the tool in a category. Open src/tools/index.ts and add your imported tool to the appropriate category's components: [...] array:
{ name: 'Development', components: [/* ... */, myToolName] },
10.3 Build the tool
- Implement the UI in
my-tool-name.vueusing thec-*UI kit and Naive UI. Remember Vue APIs and many components are auto-imported — you generally don't writeimport { ref } from 'vue'or importc-button. - Put pure, testable logic in
my-tool-name.service.tsand test it in the.test.ts. - Use shared composables where applicable:
useCopy()for copy-to-clipboard with a toast.useValidation()for input validation rules.useQueryParamOrStorage()to keep tool state in the URL + local storage (gives you shareable links and remembered settings for free).FormatTransformer.vue/TextareaCopyable.vuefor the common "input → transformed output with copy" pattern.
- Add translations: a per-tool
locales/folder, or entries undertools.<key>inlocales/en.yml(and other languages). Usetranslate('tools.<key>.title'/'.description')in the descriptor. Tools with static English text skip i18n (e.g.email-normalizer). - Set
iconto a Tabler/Material/MDI icon component. - Optionally add
redirectFromto preserve old URLs andkeywordsto improve search hits.
10.4 The Tool contract
// src/tools/tools.types.ts
export interface Tool {
name: string;
path: string;
description: string;
keywords: string[];
component: () => Promise<Component>; // lazy import — enables code splitting
icon: Component;
redirectFrom?: string[];
isNew: boolean; // computed by defineTool
createdAt?: Date;
}
Important
Always pass
componentas a lazy import (() => import('./x.vue')). This is what keeps the initial bundle small — each tool is code-split into its own chunk loaded on demand.
11. Internals Deep Dive
11.1 Favorites system
Favorites are stored as an array of tool paths in useStorage('favoriteToolsName', []). The tools store resolves these against the (i18n-projected) tool list, exposes favoriteTools, and supports add/remove/reorder. The home page uses vuedraggable to reorder favorites and persists the new order via updateFavoriteTools(newOrder). The sidebar surfaces favorites as a synthetic category above the real ones.
11.2 The "New" badge
defineTool sets isNew = isAfter(createdAt, subWeeks(now, 2)). The tools store exposes newTools, rendered both as a home-page section and as a small colored dot in the sidebar (MenuIconItem.vue) and a "New" pill on ToolCard.vue.
11.3 Command palette
command-palette.store.ts composes toolsOptions (every tool, tagged category: 'Tools') with static action options. Fuse.js searches across name (weight 2), description, keywords, and category with threshold: 0.3. Results are grouped by category and capped at 5 per group. Actions can be a route (to), an external link (href), or an imperative action() (e.g. Random tool picks _.sample(tools) and navigates; Toggle dark mode flips the style store).
11.4 Copy & toast pattern
useCopy() (src/composable/copy.ts) wraps @vueuse/core's useClipboard (legacy mode for broad support) and optionally raises a Naive UI success message. The *Copyable components (InputCopyable, TextareaCopyable, SpanCopyable) build the ubiquitous "value + copy button" UI; TextareaCopyable additionally renders syntax-highlighted output via a locally-registered highlight.js (sql/json/xml/yaml/ini/markdown languages).
11.5 State-in-URL pattern
useQueryParam/useQueryParamOrStorage (src/composable/queryParams.ts) serialize typed values (number/string/boolean/object) to and from the route query, with an optional local-storage fallback. Tools that adopt this get shareable, bookmarkable links and remembered settings automatically.
11.6 Validation pattern
useValidation() (src/composable/validation.ts) runs an array of { validator, message, getErrorMessage? } rules against a source ref and produces a reactive { isValid, message, status, attrs } suitable for binding to the c-input-text feedback props. isFalsyOrHasThrown treats a thrown exception or a nil/false return as a validation failure — convenient for "parse must succeed" rules.
11.7 Theming
src/themes.ts defines Naive UI lightThemeOverrides/darkThemeOverrides (primary green #1ea54c, menu/layout/card colors). App.vue selects the theme from the style store's isDarkTheme (driven by useDark(), which respects the OS preference and persists the choice).
11.8 Analytics plumbing
plausible.plugin.ts creates either a real Plausible(...) tracker (when config.plausible.isTrackerEnabled) or a no-op fake, then provides it. tracker.services.ts (useTracker) injects it and exposes trackEvent({ eventName }). In the analyzed source the only explicit event is "Support button clicked" (the Buy-me-a-coffee button); page views are auto-tracked via enableAutoPageviews(). With analytics off (default), all of this is inert.
12. Code Quality & Conventions
| Area | Convention |
|---|---|
| Language | TypeScript everywhere; .vue SFCs with <script setup lang="ts">. |
| Imports | Vue/router/vueuse/i18n + many components are auto-imported — don't add explicit imports for them. |
| Styling | UnoCSS atomic classes in templates; scoped <style lang="less"> for component-specific CSS. |
| Components | Prefer the c-* UI kit; fall back to Naive UI. |
| Logic separation | Pure logic in *.service.ts, UI in *.vue — keeps logic unit-testable. |
| Lint/format | ESLint with @antfu/eslint-config (+ UnoCSS rules); Prettier available. |
| i18n | User-facing strings via vue-i18n; tool titles/descriptions via translate(). |
| Type safety | Build gate: vue-tsc --noEmit runs before vite build. |
Strengths: the registry pattern makes the codebase highly uniform and easy to extend; tools are isolated; logic/UI separation aids testing; consistent UI components yield a coherent UX.
Watch-outs: heavy reliance on auto-import can confuse newcomers and some IDEs (the README documents the Volar "Take Over Mode" setup); the breadth of per-tool third-party libraries makes the dependency tree large.
13. Testing Strategy
| Layer | Tool | Location / pattern |
|---|---|---|
| Unit | Vitest (jsdom) | *.test.ts / *.service.test.ts colocated with tools and composables. |
| E2E | Playwright | *.e2e.spec.ts colocated with tools (excluded from the Vitest run). |
| Type | vue-tsc |
Enforced in the build and typecheck scripts. |
Run them:
pnpm test # unit (vitest, jsdom)
pnpm coverage # unit + coverage
pnpm test:e2e # playwright (spins up a server)
pnpm test:e2e:dev # playwright against an already-running dev server
The scaffolder seeds every new tool with a unit-test stub and an E2E stub (the latter asserts the page <title> is <Tool> - IT Tools, which the tool.layout sets via @vueuse/head). Coverage is uneven across the ~100 tools; pure service logic is the best-tested layer.
Tip
When adding a tool, put parsing/transformation logic in
*.service.tsand cover it with Vitest — it's far cheaper and more reliable than testing through the Vue component.
14. Potential Improvements
Derived from the source and community-reported issues:
- Configuration is build-time only. Self-hosters cannot toggle analytics/banners/base path without rebuilding. A small runtime-config approach (e.g. a
config.jsonfetched at startup, or env substitution intoindex.htmlat container start) would make the official image far more flexible for self-hosters. (High value for admins.) - Replace/clearly mark weak crypto. RC4 and TripleDES in the Encrypt/decrypt tool should be labeled as insecure/legacy in the UI (the docs already warn), or removed in favor of AES-GCM.
- Dependency surface. The Dependency Dashboard issue (#480) tracks keeping the large client-library set current; automated dependency updates + periodic rebuilds reduce client-side CVE exposure.
- Accessibility & input edge cases. Community issues report problems such as the ASCII art generator failing on some inputs (#1818) and regex-tester edge cases (#1814) — candidates for targeted fixes and more unit coverage.
- Favorites portability. Favorites/settings are per-browser local storage. An optional export/import (JSON) would let users move their setup between browsers/devices without a backend.
- Scaffolder finishing step.
create-tool.mjsinserts the import but not the category placement; extending it to also place the tool in a category (or prompting for one) would remove a manual step. - i18n completeness. Some non-English locales are partial; a coverage report per locale would help contributors prioritize.
15. Quick Reference
Image & ports
# Run (Docker Hub)
docker run -d --name it-tools --restart unless-stopped -p 8080:80 corentinth/it-tools:latest
# Run (GHCR)
docker run -d --name it-tools --restart unless-stopped -p 8080:80 ghcr.io/corentinth/it-tools:latest
- Container port: 80 (nginx). Common host mapping: 8080.
Build from source
pnpm install
pnpm build # → ./dist (needs ~4GB RAM)
pnpm preview # serve build on :5050
Build-time config (env, baked into the bundle)
| Env var | Default | Effect |
|---|---|---|
BASE_URL |
/ |
Serve under a sub-path. |
VITE_TRACKER_ENABLED |
false |
Enable Plausible analytics. |
VITE_PLAUSIBLE_DOMAIN |
'' |
Plausible site domain. |
VITE_PLAUSIBLE_API_HOST |
'' |
Plausible API host. |
VITE_SHOW_BANNER |
false |
Show the GitHub-star banner. |
VITE_SHOW_SPONSOR_BANNER |
false |
Show the sponsor banner. |
VITE_VERCEL_ENV |
development |
App env (production hides the UI lab). |
VITE_VERCEL_GIT_COMMIT_SHA |
'' |
Footer commit link. |
PACKAGE_VERSION |
0.0.0 |
Footer version (auto from npm_package_version). |
Add a tool
pnpm run script:create:tool my-tool-name
# then add `myToolName` to a category in src/tools/index.ts
Key files
| File | Role |
|---|---|
src/tools/index.ts |
Tool registry + categories. |
src/tools/tool.ts |
defineTool() factory. |
src/router.ts |
Routes generated from the registry. |
src/config.ts |
Configuration schema (figue, build-time env). |
vite.config.ts |
Build + plugin chain. |
Dockerfile / nginx.conf |
Production image + SPA server. |
Facts that prevent mistakes (negative knowledge)
- There is no backend, no database, no API, no server-side secrets. Don't look for them.
- Config is build-time;
docker run -e VITE_...does not change a prebuilt image. - Analytics are off by default in self-hosted builds.
- The only Vite-detected runtime/build env keys beyond the figue schema are test/CI vars (
CI,BASE_URL,NO_WEB_SERVERfromplaywright.config.ts); there are no other runtime configuration knobs. - Tool components are lazy-loaded; the app is code-split per tool.