diff --git a/.changeset/eight-bobcats-refuse.md b/.changeset/eight-bobcats-refuse.md
new file mode 100644
index 0000000..69bbca5
--- /dev/null
+++ b/.changeset/eight-bobcats-refuse.md
@@ -0,0 +1,5 @@
+---
+'@neodx/figma': minor
+---
+
+New documentation
diff --git a/.changeset/funny-knives-kiss.md b/.changeset/funny-knives-kiss.md
new file mode 100644
index 0000000..5a06fe1
--- /dev/null
+++ b/.changeset/funny-knives-kiss.md
@@ -0,0 +1,5 @@
+---
+'@neodx/log': minor
+---
+
+Rework `createLoggerFactory` levels types
diff --git a/.changeset/itchy-snails-agree.md b/.changeset/itchy-snails-agree.md
new file mode 100644
index 0000000..44a160a
--- /dev/null
+++ b/.changeset/itchy-snails-agree.md
@@ -0,0 +1,5 @@
+---
+'@neodx/svg': patch
+---
+
+Make the argument for builder params optional
diff --git a/.changeset/nasty-horses-heal.md b/.changeset/nasty-horses-heal.md
new file mode 100644
index 0000000..83c5459
--- /dev/null
+++ b/.changeset/nasty-horses-heal.md
@@ -0,0 +1,5 @@
+---
+'@neodx/svg': patch
+---
+
+Simplify generated metadata types
diff --git a/.changeset/new-geckos-wink.md b/.changeset/new-geckos-wink.md
new file mode 100644
index 0000000..b8a701c
--- /dev/null
+++ b/.changeset/new-geckos-wink.md
@@ -0,0 +1,5 @@
+---
+'@neodx/svg': minor
+---
+
+New documentation
diff --git a/.changeset/red-trees-poke.md b/.changeset/red-trees-poke.md
new file mode 100644
index 0000000..f79ef59
--- /dev/null
+++ b/.changeset/red-trees-poke.md
@@ -0,0 +1,5 @@
+---
+'@neodx/std': minor
+---
+
+Add first `typeof ...` shortcuts
diff --git a/.changeset/wet-papayas-think.md b/.changeset/wet-papayas-think.md
new file mode 100644
index 0000000..e757078
--- /dev/null
+++ b/.changeset/wet-papayas-think.md
@@ -0,0 +1,5 @@
+---
+'@neodx/log': patch
+---
+
+New documentation
diff --git a/.prettierrc.js b/.prettierrc.cjs
similarity index 100%
rename from .prettierrc.js
rename to .prettierrc.cjs
diff --git a/.yarnrc.yml b/.yarnrc.yml
index ab9564b..9a1588a 100644
--- a/.yarnrc.yml
+++ b/.yarnrc.yml
@@ -7,7 +7,7 @@ changesetIgnorePatterns:
- '**/*.test.{js,ts,tsx}'
- '**/*.spec.{js,ts,tsx}'
-defaultSemverRangePrefix: '^'
+defaultSemverRangePrefix: ''
enableGlobalCache: false
diff --git a/README.md b/README.md
index 6376286..60b667c 100644
--- a/README.md
+++ b/README.md
@@ -7,51 +7,77 @@
This project is designed to tackle common web development challenges with ease.
+Check out our [documentation](https://neodx.pages.dev) to learn more!
+
> **Warning**
> Most of the packages are still under development, so API may change.
> I'll try to keep it stable, but updates still can bring breaking changes.
+Packages overview:
+
+- [@neodx/figma](#neodxfigma) | [docs](https://neodx.pages.dev/figma) | [source](./libs/figma)
+- [@neodx/svg](#neodxsvg) | [docs](https://neodx.pages.dev/svg) | [source](./libs/svg)
+- [@neodx/log](#neodxlog) | [docs](https://neodx.pages.dev/log) | [source](./libs/log)
+
### [@neodx/figma](./libs/figma)
Figma is a great tool for design collaboration, but we don't have a solid way to use it in our development workflow.
+#### But we have a problem!
+
Probably, you've already tried to write your own integration or use some existing solutions and faced the following problems as me:
-- Multiple different not maintained packages with different APIs
-- Bad documentation/usage examples or even no documentation at all
-- Terrible flexibility and solution design, you just can't use it in your project because of the different document structure or workflow
-- No type safety, autocomplete, etc.
+- ðĪŊ Multiple different not maintained packages with different APIs
+- ðŦ Bad documentation/usage examples or even no documentation at all
+- ð Terrible flexibility and solution design, you just can't use it in your project because of the different document structure or workflow
+- ð ââïļ No type safety, autocomplete, etc.
In other words, there is no really well-designed complex solution for Figma integration.
+#### Let's solve it!
+
So, `@neodx/figma` is an attempt to create it. Currently, we have the following features:
- **Flexible Export CLI**: You can use it to export icons or other elements. It's a simple wrapper around our Node.JS API.
- **Typed Figma API**: All Figma API methods are typed and have autocomplete support.
- **Built-in document graph API**: Figma API is too low-level for writing any stable solution. We provide an API that allows you work with the document as a simple high-level graph of nodes.
+[Visit `@neodx/figma` documentation](https://neodx.pages.dev/svg) to learn more!
+
See our examples for more details:
- [SVG sprite generation on steroids with Figma export](./examples/svg-magic-with-figma-export) - Integrated showcase of the `@neodx/svg` and `@neodx/figma` packages with real application usage!
- [Export icons from the Community Weather Icons Kit](./examples/figma-export-file-assets) - A simple step-by-step example of how to use the `@neodx/figma` to export icons.
-We have a some ideas for future development, so stay tuned and feel free to request your own! ð
+Also, we have some ideas for future development, so stay tuned and feel free to request your own! ð
### [@neodx/svg](./libs/svg)
+Supercharge your icons âĄïļ
+
Are you converting every SVG icon to a React component with SVGR or something similar? It's so ease to use!
But wait; did you know that SVG sprites are a native approach for icons? It's even easier to use!
```typescript jsx
-import { Icon } from '@/shared/ui';
+import { Icon, type AnyIconName } from '@/shared/ui/icon';
+
+export interface MyComponentProps {
+ icon: AnyIconName;
+}
-export const MyComponent = () => (
+export const MyComponent = ({ icon }: MyComponentProps) => (
<>
-
-
+ {/* Use as is */}
+
+ {/* Change color */}
+
+ {/* Change size */}
+ {/* Add any other styles */}
+ {/* Use simple type-safe names instead of weird components */}
+
>
);
```
@@ -61,143 +87,15 @@ No runtime overhead, one component for all icons, native browser support, static
Sounds good? Of course! Native sprites are unfairly deprived technique.
Probably, you already know about it, but didn't use it because of the lack of tooling.
-Here we go! Type safety, autocomplete, runtime access to icon metadata all wrapped in simple plugins for all popular bundlers (thx [unplugin](https://github.com/unjs/unplugin)) and CLI, for example:
-
-
- Vite plugin
-
-```typescript
-import { defineConfig } from 'vite';
-import svg from '@neodx/svg/vite';
-
-export default defineConfig({
- plugins: [
- svg({
- root: 'assets',
- output: 'public',
- metadata: 'src/shared/ui/icon/sprite.gen.ts'
- })
- ]
-});
-```
-
-
+Here we go! Type safety, autocomplete, runtime access to icon metadata all wrapped in simple plugins for all popular bundlers.
-
- Webpack plugin
-
-```typescript
-import svg from '@neodx/svg/webpack';
-
-export default {
- plugins: [
- svg({
- root: 'assets',
- output: 'public',
- metadata: 'src/shared/ui/icon/sprite.gen.ts'
- })
- ]
-};
-```
-
-
-
-
- Rollup plugin
-
-```typescript
-import svg from '@neodx/svg/rollup';
-
-export default {
- plugins: [
- svg({
- root: 'assets',
- output: 'public',
- metadata: 'src/shared/ui/icon/sprite.gen.ts'
- })
- ]
-};
-```
-
-
-
-
- ESBuild plugin
-
-```typescript
-import svg from '@neodx/svg/esbuild';
-
-export default {
- plugins: [
- svg({
- root: 'assets',
- output: 'public',
- metadata: 'src/shared/ui/icon/sprite.gen.ts'
- })
- ]
-};
-```
+[Visit `@neodx/svg` documentation](https://neodx.pages.dev/svg) to learn more!
-
-
-
- CLI
-
-```shell
-npx @neodx/svg --group --root assets --output public --definition src/shared/ui/icon/sprite.gen.ts
-# --root - root folder with SVGs
-# --group - group icons by folders (assets/common/add.svg -> common/add, assets/other/cut.svg -> other/cut)
-# --output (-o) - output folder for sprites
-# --definition (-d) - output file for sprite definitions
-```
-
-
-
-
-Node.JS API (programmatic usage, low-level)
-
-```typescript
-import { buildSprites } from '@neodx/svg';
-import { createVfs } from '@neodx/vfs';
-
-await buildSprites({
- vfs: createVfs(process.cwd()),
- root: 'assets',
- input: '**/*.svg',
- output: 'public',
- metadata: 'src/shared/ui/icon/sprite.gen.ts'
-});
-```
-
-
-
-For the details and real usage see our examples:
+Also, you can check out our examples:
- [React, Vite, TailwindCSS, and multicolored icon](./examples/svg-vite) - A step-by-step tutorial showcasing how to integrate sprite icons into your Vite project.
- [React, Vite, icons exported by "@neodx/figma"](./examples/svg-magic-with-figma-export) - Integrated showcase of the seamless automation capabilities of `@neodx/svg` and `@neodx/figma` for your icons!
-- [NextJS, webpack and simple flat icons](./examples/svg-next) - An example demonstrating the usage of `@neodx/svg` webpack plugin with NextJS.
-
-In the result, you'll get something like this:
-
-```diff
-...
-src/
- shared/
- ui/
- icon/
-+ sprite.gen.ts // sprite definitions - types, metadata, etc.
-public/
-+ sprite/
-+ common.svg
-+ other.svg
-assets/
- common/
- add.svg
- close.svg
- other/
- cut.svg
- search.svg
-```
+- [Next.js, webpack and simple flat icons](./examples/svg-next) - An example demonstrating the usage of `@neodx/svg` webpack plugin with Next.js.
### [@neodx/log](./libs/log)
@@ -298,26 +196,52 @@ async function doSomethingWithVfs(vfs: Vfs) {
While it may seem unnecessary at first glance, let's explore the core concepts that make `@neodx/vfs` invaluable:
- Single abstraction - just share `Vfs` instance between all parts of your tool
-- Inversion of control - you can provide any implementation of `Vfs` interface
+- Inversion of control: you can provide any implementation of `Vfs` interface
- Btw, we already have built-in support in the `createVfs` API
-- Dry-run mode - you can test your tool without any side effects (only in-memory changes, read-only FS access)
-- Virtual mode - you can test your tool without any real file system access, offering in-memory emulation for isolated testing
+- Dry-run mode: you can test your tool without any side effects (only in-memory changes, read-only FS access)
+- Virtual mode: you can test your tool without any real file system access, offering in-memory emulation for isolated testing
- Attached working directory - you can use clean relative paths in your logic without any additional logic
-- Extensible - you can build your own features on top of `Vfs` interface
+- Extensible: you can build your own features on top of `Vfs` interface
- Currently, we have built-in support for formatting files, updating JSON files, and package.json dependencies management
In other words, it's designed as single API for all file system operations, so you can focus on your tool logic instead of reinventing the wheel.
## Development and contribution
+### Getting started
+
+We're using [Yarn 3 (berry)](https://yarnpkg.com/) as a package manager and [Nx](https://nx.dev/) as a monorepo management tool.
+
+After cloning the repo, to install dependencies, run:
+
+```shell
+yarn
+```
+
+And, optionally, for building all packages, run:
+
+```shell
+yarn nx run-many --all --target=build
+```
+
+It isn't necessary, you can start working with the codebase right away, but it will boost initial cache whn you run e2e tests (scenarios in examples/\*).
+
### Internal scripts
-#### Create new library
+#### Create a new global example
```shell
-yarn neodx lib my-lib-name
+yarn neodx example new-example-name
```
+#### Create a new library
+
+```shell
+yarn neodx lib new-lib-name
+```
+
+The source code can be accessed by starting from [tools/scripts/bin.mjs](./tools/scripts/bin.mjs)..
+
## License
Licensed under the [MIT License](./LICENSE).
diff --git a/apps/docs/.vitepress/config.ts b/apps/docs/.vitepress/config.ts
index 860b028..25eb3df 100644
--- a/apps/docs/.vitepress/config.ts
+++ b/apps/docs/.vitepress/config.ts
@@ -4,7 +4,19 @@ import { defineConfig } from 'vitepress';
export default defineConfig({
title: 'Neodx',
description: 'Modern solutions for great DX',
+ head: [
+ ['link', { rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png' }],
+ ['link', { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32x32.png' }],
+ ['link', { rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicon-16x16.png' }],
+ ['link', { rel: 'manifest', href: '/site.webmanifest' }],
+ ['link', { rel: 'mask-icon', href: '/safari-pinned-tab.svg', color: '#710ab7' }],
+ ['link', { rel: 'shortcut icon', href: '/favicon.ico' }],
+ ['meta', { name: 'msapplication-TileColor', content: '#603cba' }],
+ ['meta', { name: 'msapplication-config', content: '/browserconfig.xml' }],
+ ['meta', { name: 'theme-color', content: '#ffffff' }]
+ ],
themeConfig: {
+ logo: '/logo.png',
socialLinks: [{ icon: 'github', link: 'https://github.com/secundant/neodx' }],
footer: {
message: 'Released under the MIT License.',
@@ -28,11 +40,39 @@ export default defineConfig({
collapsed: true,
items: [
{ text: 'Getting started', link: '/log/' },
- { text: 'Pretty printing', link: '/log/pretty-printing' },
- { text: 'JSON logs', link: '/log/json' },
- { text: 'Children and forks', link: '/log/child-and-fork' },
- { text: 'HTTP frameworks', link: '/log/http-frameworks' },
- { text: 'Creating your own logger', link: '/log/custom' }
+ { text: 'Motivation', link: '/log/motivation' },
+ { text: 'Formatting', link: '/log/formatting' },
+ { text: 'Metadata', link: '/log/metadata' },
+ { text: 'Forked and child loggers', link: '/log/fork-and-child' },
+ { text: 'Creating your own logger', link: '/log/building-your-own-logger' },
+ {
+ text: 'Targets',
+ items: [
+ { text: 'JSON logs', link: '/log/targets/json' },
+ { text: 'Pretty format', link: '/log/targets/pretty' }
+ ]
+ },
+ {
+ text: 'Frameworks',
+ link: '/log/frameworks/',
+ items: [
+ { text: 'Express', link: '/log/frameworks/express' },
+ { text: 'Koa', link: '/log/frameworks/koa' },
+ { text: 'Node.js http', link: '/log/frameworks/http' }
+ ]
+ },
+ {
+ text: 'API',
+ collapsed: true,
+ items: [
+ { text: 'createLogger', link: '/log/api/create-logger' },
+ { text: 'createLoggerFactory', link: '/log/api/create-logger-factory' },
+ { text: 'logger', link: '/log/api/logger' },
+ { text: 'printf', link: '/log/api/printf' },
+ { text: 'readArguments', link: '/log/api/read-arguments' },
+ { text: '@neodx/log/http', link: '/log/api/http' }
+ ]
+ }
]
},
{
@@ -41,8 +81,48 @@ export default defineConfig({
items: [
{ text: 'Getting started', link: '/svg/' },
{ text: 'Motivation', link: '/svg/motivation' },
- { text: 'Frameworks and bundlers', link: '/svg/frameworks-and-bundlers' },
- { text: 'Automatically reset colors', link: '/svg/colors-reset' }
+ {
+ text: 'Setup',
+ link: '/svg/setup/',
+ items: [
+ { text: 'Vite', link: '/svg/setup/vite.md' },
+ { text: 'Next', link: '/svg/setup/next.md' },
+ { text: 'Webpack', link: '/svg/setup/webpack.md' },
+ { text: 'Other', link: '/svg/setup/other' }
+ ]
+ },
+ {
+ text: 'Guides',
+ items: [
+ { text: 'Group and hash sprites', link: '/svg/group-and-hash' },
+ { text: 'Generate metadata', link: '/svg/metadata' },
+ { text: 'âĻ Writing Icon component', link: '/svg/writing-icon-component' },
+ { text: 'Automatically reset colors', link: '/svg/colors-reset' },
+ { text: 'Working with multicolored', link: '/svg/multicolored' }
+ ]
+ },
+ {
+ text: 'API',
+ collapsed: true,
+ link: '/svg/api/',
+ items: [
+ { text: 'createSpritesBuilder', link: '/svg/api/create-sprites-builder' },
+ { text: 'createWatcher', link: '/svg/api/create-watcher' },
+ { text: 'buildSprites', link: '/svg/api/build-sprites' },
+ {
+ text: 'Plugins API',
+ collapsed: true,
+ items: [
+ { text: 'resetColors', link: '/svg/api/plugins/reset-colors' },
+ {
+ text: 'metadata',
+ link: '/svg/api/plugins/metadata'
+ },
+ { text: 'svgo', link: '/svg/api/plugins/svgo' }
+ ]
+ }
+ ]
+ }
]
},
{
@@ -69,6 +149,7 @@ export default defineConfig({
{
text: 'Export API',
link: '/figma/api/export/',
+ collapsed: true,
items: [
{ text: 'Export File Assets', link: '/figma/api/export/export-file-assets' },
{
diff --git a/apps/docs/.vitepress/theme/style.css b/apps/docs/.vitepress/theme/style.css
index 55a3eb1..c6ddc1e 100644
--- a/apps/docs/.vitepress/theme/style.css
+++ b/apps/docs/.vitepress/theme/style.css
@@ -8,12 +8,15 @@
* -------------------------------------------------------------------------- */
:root {
- --vp-c-brand: #646cff;
- --vp-c-brand-light: #747bff;
- --vp-c-brand-lighter: #9499ff;
- --vp-c-brand-lightest: #bcc0ff;
- --vp-c-brand-dark: #535bf2;
- --vp-c-brand-darker: #454ce1;
+ --vp-c-brand: #d5a71a;
+ --vp-c-brand-light: #debb44;
+ --vp-c-brand-1: #d29b0f;
+ --vp-c-brand-2: #debb44;
+ --vp-c-brand-3: #b67e09;
+ --vp-c-brand-lighter: #ead869;
+ --vp-c-brand-lightest: #eee39d;
+ --vp-c-brand-darker: #b67e09;
+ --vp-c-brand-dark: #d29b0f;
--vp-c-brand-dimm: rgba(100, 108, 255, 0.08);
}
@@ -39,9 +42,9 @@
:root {
--vp-home-hero-name-color: transparent;
- --vp-home-hero-name-background: -webkit-linear-gradient(120deg, #bd34fe 30%, #41d1ff);
+ --vp-home-hero-name-background: -webkit-linear-gradient(120deg, #eebe13 30%, #41d1ff);
- --vp-home-hero-image-background-image: linear-gradient(-45deg, #bd34fe 50%, #47caff 50%);
+ --vp-home-hero-image-background-image: linear-gradient(-45deg, #eebe13 50%, #47caff 50%);
--vp-home-hero-image-filter: blur(40px);
}
diff --git a/apps/docs/figma/motivation.md b/apps/docs/figma/motivation.md
index 706d83b..df2d4c2 100644
--- a/apps/docs/figma/motivation.md
+++ b/apps/docs/figma/motivation.md
@@ -3,7 +3,17 @@
I've been looking for a stable maintaining Figma API with built-in high-level structures, abstractions, and common features for implementing high-level tools for Figma.
But I didn't find anything that would suit me, [figma-js](https://github.com/jemgold/figma-js) and [figma-api](https://github.com/didoo/figma-api) both are not maintained, low-level API only and depends on the axios library, which is not suitable for me.
-Next, I found [figma-transformer](https://github.com/figma-tools/figma-transformer), nice project for creating a human-friendly data structure, but it's not a full-featured, not maintained, and written in **very** unsafe and untyped style.
+Next, I found [figma-transformer](https://github.com/figma-tools/figma-transformer), a nice project for creating a human-friendly data structure.
+However, it's not a full-featured, not maintained, and written in a very unsafe and untyped style.
+
+Probably, you've already tried to write your own integration or use some of the above solutions and faced the same problems as I did.
+
+- ðĪŊ Multiple different not maintained packages with different APIs
+- ðŦ Bad documentation/usage examples or even no documentation at all
+- ð Terrible flexibility and solution design, you just can't use it in your project because of the different document structure or workflow
+- ð ââïļ No type safety, autocomplete, etc.
+
+In other words, there is no really well-designed complex solution for Figma integration.
So I decided to create my own Figma API, which will be:
diff --git a/apps/docs/index.md b/apps/docs/index.md
index 5aab14d..280e898 100644
--- a/apps/docs/index.md
+++ b/apps/docs/index.md
@@ -5,7 +5,6 @@ layout: home
hero:
name: 'Neodx'
text: 'Modern solutions for great DX'
- tagline: Documentation is under construction...
features:
- title: '@neodx/log'
diff --git a/apps/docs/log/api/create-logger-factory.md b/apps/docs/log/api/create-logger-factory.md
new file mode 100644
index 0000000..df38a71
--- /dev/null
+++ b/apps/docs/log/api/create-logger-factory.md
@@ -0,0 +1,38 @@
+# `createLoggerFactory`
+
+Creates new [createLogger](./create-logger.md) factory with predefined behavior.
+
+```typescript
+declare function createLoggerFactory(
+ options: CreateLoggerFactoryParams
+): CreateLogger>;
+```
+
+## `CreateLoggerFactoryParams`
+
+- [readArguments](./read-arguments.md) creates semantic information about log arguments from raw arguments list
+- [formatMessage](./printf.md) formats message template with user arguments. Default implementation is lightweight printf-like function
+- `defaultParams` are used as default values for all loggers created by this factory
+
+```typescript
+interface CreateLoggerFactoryParams {
+ defaultParams: LoggerParamsWithLevels;
+
+ readArguments(args: unknown[]): LogArguments;
+
+ /**
+ * Formats a message template with replaces.
+ * @default Our lightweight implementation with %s, %d, %i, %f, %j/%o/%O (same output as %j) support
+ * @example Node.js util.format
+ * (template, replaces) => util.format(template, ...replaces)
+ */
+ formatMessage(template: string, replaces: unknown[]): string;
+}
+```
+
+## Related
+
+- [`createLogger` API](./create-logger.md)
+- [Building your own logger](../building-your-own-logger.md)
+- [printf](./printf.md)
+- [readArguments](./read-arguments.md)
diff --git a/apps/docs/log/api/create-logger.md b/apps/docs/log/api/create-logger.md
new file mode 100644
index 0000000..49d4c4d
--- /dev/null
+++ b/apps/docs/log/api/create-logger.md
@@ -0,0 +1,192 @@
+# `createLogger`
+
+Create and configure a new logger instance.
+
+Can be used directly from `@neodx/log` or `@neodx/log/node` imports
+or [built by yourself](../building-your-own-logger.md) with [createLoggerFactory](./create-logger-factory.md).
+
+Returns [Logger](./logger.md) instance.
+
+- [LoggerParams](#loggerparams)
+- [DefaultLevel](#defaultlevel)
+
+```typescript
+// Use default levels
+declare function createLogger(options?: Partial>): Logger;
+
+// Override default levels
+declare function createLogger(
+ params: LoggerParamsWithLevels
+): Logger>;
+
+const logger = createLogger({ name: 'my-app' });
+// ^? Logger
+const custom = createLogger({ levels: { foo: 10, bar: 20 }, level: 'foo' });
+// ^? Logger<'foo' | 'bar'>
+```
+
+## `LoggerParams`
+
+Logger configuration object.
+
+- [LoggerTransformer](#loggertransformer)
+- [LoggerHandler](#loggerhandler)
+- [LoggerHandleConfig](#loggerhandleconfig)
+
+```typescript
+export interface LoggerParams {
+ /**
+ * Logger name will be shown in the logs.
+ * @example 'my-app'
+ * @example 'my-app:my-module'
+ */
+ name: string;
+ /**
+ * The logging level, everything higher than this level will be ignored.
+ * @example 'info'
+ * @example 'verbose'
+ */
+ level: Level;
+ /**
+ * Additional fields that will be added to every log chunk.
+ * @example { pid: process.pid }
+ */
+ meta: Record;
+ /**
+ * List of streams that will receive log chunks.
+ * @example [{ level: 'info', target: [json] }, { level: 'error', target: [file] }]
+ * @example [{ level: 'info', target: json }, { level: 'error', target: file }]
+ * @example [json]
+ * @example json
+ * @example { level: 'info', target: json }
+ * @example { level: 'info', target: [json] }
+ * @example { level: 'info', target: [{ write: json }] }
+ */
+ target:
+ | LoggerHandler
+ | LoggerHandleConfig
+ | Array | LoggerHandleConfig | Falsy>;
+ transform: LoggerTransformer | LoggerTransformer[];
+}
+```
+
+## `LoggerHandler`
+
+[LogChunk](#logchunk) receiver that implements the actual logging logic (e.g. writes to console, sends to server, writes to file, etc.).
+
+It Could be an async function, but it won't be handled by the logger itself, so you should handle async errors by yourself.
+
+```typescript
+export interface LoggerHandler {
+ (chunk: LogChunk): void | Promise;
+}
+```
+
+## `LoggerHandleConfig`
+
+Extended handler definition that allows to specify minimum level priority for the handler.
+
+- [LoggerHandler](#loggerhandler)
+
+```typescript
+export interface LoggerHandleConfig {
+ /**
+ * The minimum level priority that this stream will receive.
+ * @example 'info' - will receive 'info', 'warn' and 'error' chunks
+ * @example 'warn' - will receive 'warn' and 'error' chunks
+ * @example 'error' - will receive only 'error' chunks
+ * @default no minimum level, will receive all chunks
+ */
+ level?: Level;
+ /**
+ * Your handler function(s) that will receive log chunks.
+ * @example (chunk) => console.log(chunk)
+ * @example (chunk) => Promise.resolve(console.log(chunk))
+ */
+ target: LoggerHandler | LoggerHandler[];
+}
+```
+
+## `LoggerTransformer`
+
+Custom transformer function that will receive log chunks before they are passed to streams.
+
+- [LogChunk](#logchunk)
+
+```typescript
+/**
+ * @example chunk => ({ ...chunk, msg: chunk.msg.toUpperCase() }) // uppercase all messages :)
+ */
+export interface LoggerTransformer {
+ (chunk: LogChunk): LogChunk;
+}
+```
+
+## `LogChunk`
+
+Aggregated log data object that will be passed to transformers for preprocessing and then to streams for output.
+
+```typescript
+export interface LogChunk {
+ /**
+ * The name of the logger that created this chunk.
+ * @example 'my-app'
+ * @example 'my-app:my-module'
+ */
+ name: string;
+ /**
+ * The date that this chunk was created.
+ */
+ date: Date;
+ /**
+ * The level of this chunk.
+ * @example 'info'
+ * @example 'warn'
+ */
+ level: Level;
+ /**
+ * The error that was passed as first argument to the log method (usually at `error` level).
+ */
+ error?: Error;
+ /**
+ * Object with additional fields that were passed to the log method.
+ * @example { pid: 1234, hostname: 'my-host' }
+ * @example { headers: { 'x-request-id': '1234' } }
+ */
+ meta: LoggerBaseMeta;
+ /**
+ * Pre-formatted message.
+ * @example "Value of 'foo' is 123"
+ */
+ msg: string;
+ /**
+ * Message arguments that were passed to the log method.
+ * @example ['foo', 123]
+ */
+ msgArgs?: unknown[];
+ /**
+ * Message template that was passed to the log method.
+ * @example "Value of '%s' is %d"
+ */
+ msgTemplate?: string;
+}
+```
+
+## `DefaultLevel`
+
+Default logging level literal type.
+
+```typescript
+export type DefaultLevel =
+ // Core levels (with their weights)
+ | 'error' // 10
+ | 'warn' // 20
+ | 'info' // 30
+ | 'done' // 40
+ | 'debug' // 50
+ // Aliases
+ | 'success' // === 'done'
+ | 'verbose' // === 'debug'
+ // Special
+ | 'silent'; // disables all logging
+```
diff --git a/apps/docs/log/api/http.md b/apps/docs/log/api/http.md
new file mode 100644
index 0000000..3641d04
--- /dev/null
+++ b/apps/docs/log/api/http.md
@@ -0,0 +1,133 @@
+# `@neodx/log/http` API
+
+`@neodx/log/http` is a core module for logging HTTP requests and responses.
+
+## `createHttpLogger`
+
+Creates universal HTTP handler for logging requests and responses.
+
+Could be used with any Node.js HTTP server framework.
+
+- [HttpLoggerParams](#httploggerparams)
+
+```typescript
+declare function createHttpLogger<
+ Req extends IncomingMessage = IncomingMessage,
+ Res extends OutgoingMessage = OutgoingMessage
+>(params?: HttpLoggerParams): (req: Req, res: Res, next?: NextFunction) => void;
+```
+
+## `HttpLoggerParams`
+
+- [Logger](./logger.md)
+- [HttpResponseContext](#httpresponsecontext)
+- `IncomingMessage` and `OutgoingMessage` are core [Node.js HTTP module APIs](https://nodejs.org/api/http.html#class-httpincomingmessage)
+
+```typescript
+export interface HttpLoggerParams<
+ Req extends IncomingMessage = IncomingMessage,
+ Res extends OutgoingMessage = OutgoingMessage
+> {
+ /**
+ * Custom logger instance.
+ * @default createLogger()
+ */
+ logger?: Logger;
+ /**
+ * Custom colors instance
+ * @see `@neodx/colors`
+ */
+ colors?: Colors;
+ /**
+ * If `true`, the logger will only log the pre-formatted message without any additional metadata.
+ * @default process.env.NODE_ENV === 'development'
+ */
+ simple?: boolean;
+ /**
+ * Optional function to extract/create request ID.
+ * @default built-in simple safe number counter
+ */
+ getRequestId?: (req: Req, res: Res) => string | number;
+
+ // ===
+ // Metadata and formatting
+ // ===
+
+ /**
+ * Extract shared metadata for every produced log
+ */
+ getMeta?: (req: Req, res: Res) => Record;
+ /**
+ * Extract metadata for request logs
+ */
+ getRequestMeta?: (ctx: HttpResponseContext) => Record;
+ /**
+ * Custom incoming request message formatter
+ */
+ getRequestMessage?: (ctx: HttpResponseContext) => string;
+ /**
+ * Extract metadata for success response logs
+ */
+ getResponseMeta?: (ctx: HttpResponseContext) => Record;
+ /**
+ * Custom success response message formatter
+ */
+ getResponseMessage?: (ctx: HttpResponseContext) => string;
+ /**
+ * Extract metadata for error response logs
+ */
+ getErrorMeta?: (ctx: HttpResponseContext) => Record;
+ /**
+ * Custom error response message formatter
+ */
+ getErrorMessage?: (ctx: HttpResponseContext) => string;
+
+ // ===
+ // Control logging behavior
+ // ===
+
+ /**
+ * Whether to log anything at all.
+ * @default true
+ */
+ shouldLog?: boolean | ((req: Req, res: Res) => boolean);
+ /**
+ * Prevents logging of errors.
+ * @default true
+ */
+ shouldLogError?: boolean | ((ctx: HttpResponseContext) => boolean);
+ /**
+ * Prevents built-in logging of requests.
+ * DISABLED BY DEFAULT, because it can be very verbose.
+ * @default false
+ */
+ shouldLogRequest?: boolean | ((ctx: HttpResponseContext) => boolean);
+ /**
+ * Prevents built-in logging of responses.
+ * @default true
+ */
+ shouldLogResponse?: boolean | ((ctx: HttpResponseContext) => boolean);
+}
+```
+
+### `HttpResponseContext`
+
+```typescript
+export interface HttpResponseContext<
+ Req extends IncomingMessage = IncomingMessage,
+ Res extends OutgoingMessage = OutgoingMessage
+> {
+ req: Req;
+ res: Res;
+ error?: Error;
+ logger: Logger;
+ colors: Colors;
+ responseTime: number;
+}
+```
+
+## Related
+
+- [`@neodx/log/http` guide](../frameworks/http.md)
+- [`@neodx/log/express` guide](../frameworks/express.md)
+- [`@neodx/log/koa` guide](../frameworks/koa.md)
diff --git a/apps/docs/log/api/logger.md b/apps/docs/log/api/logger.md
new file mode 100644
index 0000000..83c68bc
--- /dev/null
+++ b/apps/docs/log/api/logger.md
@@ -0,0 +1,70 @@
+# `Logger` API Reference
+
+Logger instance returned by [createLogger](./create-logger.md) function.
+
+## `Logger`
+
+```typescript
+export type Logger = {
+ /**
+ * Default logger metadata (any object)
+ */
+ readonly meta: LoggerBaseMeta;
+ /**
+ * `.fork()` returns a new logger instance with the merged (NOT DEEP) params.
+ * Should be used to inherit/copy/override logger params.
+ */
+ fork
>(params?: Partial
): Logger;
+ fork(
+ params: LoggerParamsWithLevels
+ ): Logger>;
+ /**
+ * `.child()` is equal to `.fork()`, but it's merging logger's name, passed as the first argument.
+ * Should be used to create nested loggers.
+ */
+ child
>(
+ name: string,
+ params?: Partial>
+ ): Logger;
+ child(
+ name: string,
+ params: Omit, 'name'>
+ ): Logger>;
+};
+```
+
+## `LoggerMethods`
+
+Just a record of methods, for each level (or alias) there is a corresponding method:
+
+```typescript
+export type LoggerMethods = Record;
+```
+
+Could be used to define a custom logger type in your application code which isn't forced to use `@neodx/log` only:
+
+```typescript
+function calculateSomething(logger: LoggerMethods<'info' | 'error'>) {
+ logger.info('Calculating...');
+ // ...
+ try {
+ logger.info('Done!');
+ // ...
+ } catch (error) {
+ logger.error(error);
+ }
+}
+```
+
+## `LoggerMethod`
+
+Unified logger method contract.
+At the current moment it doesn't have any strict constraints, but it could be changed in the future.
+
+```typescript
+export interface LoggerMethod {
+ (target: T, message?: string, ...args: unknown[]): void;
+ (target: unknown, message?: string, ...args: unknown[]): void;
+ (message: string, ...args: unknown[]): void;
+}
+```
diff --git a/apps/docs/log/api/pretty.md b/apps/docs/log/api/pretty.md
new file mode 100644
index 0000000..7b75fe3
--- /dev/null
+++ b/apps/docs/log/api/pretty.md
@@ -0,0 +1,100 @@
+# `pretty`
+
+- [PrettyTargetParams](#prettytargetparams)
+- [Default colors and badges](#default-colors-and-badges)
+
+```typescript
+declare function pretty(
+ params?: PrettyTargetParams
+): Target;
+
+pretty.defaultColors = {
+ /* ... */
+};
+pretty.defaultBadges = {
+ /* ... */
+};
+```
+
+## `PrettyTargetParams`
+
+```typescript
+interface PrettyTargetParams {
+ /**
+ * Default handler for log messages without errors.
+ * @default console.log
+ */
+ log?(...args: unknown[]): void;
+
+ /**
+ * Default handler for log messages with errors (e.g. `logger.error(new Error(), 'message')`).
+ * @default console.error
+ */
+ logError?(...args: unknown[]): void;
+ /**
+ * Custom implementation of colors from `@neodx/colors` or other libraries with same contracts.
+ * @example
+ * createColors(false, false) // disable colors completely
+ */
+ colors?: Colors;
+ /**
+ * Display milliseconds in log message (e.g. `12:34:56.789`).
+ * Works only if `displayTime` is `true`.
+ * @default false
+ */
+ displayMs?: boolean;
+ /**
+ * Display time in a log message
+ * @default true
+ */
+ displayTime?: boolean;
+ /**
+ * Display log level in log message.
+ * @default true
+ */
+ displayLevel?: boolean;
+ /**
+ * Pretty errors configuration (true - enable default, false - disable, object - custom options).
+ * @default true
+ */
+ prettyErrors?: boolean | Partial;
+ /**
+ * Map with colorr names for each log level.
+ * @example
+ * { ...pretty.defaultColors, fatal: 'redBright' }
+ */
+ levelColors?: Partial> | null;
+ /**
+ * Map with badges for each log level.
+ * @example
+ * { ...pretty.defaultBadges, fatal: 'ð' }
+ */
+ levelBadges?: Partial> | null;
+}
+```
+
+## Default colors and badges
+
+```typescript
+const defaultLevelBadges = {
+ info: 'â',
+ done: 'â',
+ warn: 'â ',
+ error: 'â',
+ debug: 'â'
+};
+
+const defaultLevelColors = {
+ info: 'cyanBright',
+ warn: 'yellowBright',
+ done: 'greenBright',
+ debug: 'blueBright',
+ error: 'red',
+ verbose: 'bold'
+};
+```
+
+## Related
+
+- [Formatting](../formatting.md)
+- [Pretty logs](../targets/pretty.md)
diff --git a/apps/docs/log/api/printf.md b/apps/docs/log/api/printf.md
new file mode 100644
index 0000000..57bd292
--- /dev/null
+++ b/apps/docs/log/api/printf.md
@@ -0,0 +1,33 @@
+# `printf`
+
+A **limited (_see further_)** [printf](https://en.wikipedia.org/wiki/Printf_format_string) format implementation for log messages.
+You can annotate string with special placeholders, which will be replaced with values from the argument list:
+
+- `%s` - string
+- `%d` - number
+- `%i` - integer
+- `%f` - float
+- `%j` - JSON, under the hood we're resolving circular references (they will be replaced with `"[Circular]"`)
+- `%o`, `%O` - object, in our implementation, it's the same as `%j`
+- `%%` - percent sign
+
+## Reference
+
+```typescript
+declare function printf(template: string, replaces: unknown[]): string;
+```
+
+## Example
+
+```typescript
+import { printf } from '@neodx/log/utils';
+
+printf('String: %s, Number: %d, Float: %f, JSON: %j', ['string', 42, 3.14, { foo: 'bar' }]);
+// String: string, Number: 42, Float: 3.14, JSON: {"foo":"bar"}
+```
+
+## Related
+
+- [`createLoggerFactory` API](./create-logger-factory.md)
+- [Formatting](../formatting.md)
+- Inspired by [pff](https://github.com/floatdrop/pff)
diff --git a/apps/docs/log/api/read-arguments.md b/apps/docs/log/api/read-arguments.md
new file mode 100644
index 0000000..3d929bd
--- /dev/null
+++ b/apps/docs/log/api/read-arguments.md
@@ -0,0 +1,65 @@
+# `readArguments`
+
+Read an argument array and extract [metadata](../metadata.md), error and message arguments.
+
+## Reference
+
+- [LogArguments](#logarguments)
+
+```typescript
+declare function readArguments(args: unknown[]): LogArguments;
+```
+
+## Constraints
+
+- `args` must be an array of arguments.
+- The first argument must be a string, an error, or an object
+- If the first argument is a string, all arguments will be treated as message template arguments, `meta` will be an empty object
+- If the first argument is an error, `meta` will be an empty object, `error` will be an error,
+ and other arguments will be treated as message template arguments
+- If the first argument is an object
+ - If it has `err` field, `meta` will be an object with all fields except `err`, `error` will be an error, other arguments will be treated as message template arguments
+ - Otherwise, `meta` will be an object with all fields, `error` will be undefined, other arguments will be treated as message template arguments
+
+## Examples
+
+```typescript
+// Strings
+
+readArguments('hello'); // -> [ ['hello'], {} ]
+readArguments('hello %s', 'world'); // -> [ ['hello %s', 'world'], {} ]
+readArguments('hello %s %d %j', 'world', 1, { id: 2 }); // -> [ ['hello %s %d %j', 'world', 1, { id: 2 }], {} ]
+
+// Additional fields
+
+readArguments({ id: 2 }); // -> [ [], { id: 2 } ]
+readArguments({ id: 2 }, 'hello'); // -> [ ['hello'], { id: 2 } ]
+readArguments({ id: 2 }, 'hello %s', 'world'); // -> [ ['hello %s', 'world'], { id: 2 } ]
+
+// Errors
+
+readArguments(myError); // -> [ ['my error'], {}, myError ]
+readArguments({ err: myError }); // -> [ ['my error'], {}, myError ]
+readArguments({ err: myError, id: 2 }); // -> [ ['my error'], { id: 2 }, myError ]
+readArguments({ err: myError, id: 2 }, 'hello'); // -> [ ['hello'], { id: 2 }, myError ]
+readArguments({ err: myError, id: 2 }, 'hello %s', 'world'); // -> [ ['hello %s', 'world'], { id: 2 }, myError ]
+```
+
+## `LogArguments`
+
+Compact tuple parsed log arguments:
+
+1. `messageFragments` - array of message fragments, where a first element is expected to be a [format template](../formatting.md)
+ which will be passed to [printf](./printf.md) function with the rest of the arguments as a replacement list
+2. `meta` - object with fields, which will be merged with the first argument
+3. `error` - can contain an error
+
+```typescript
+export type LogArguments = [messageFragments: unknown[], meta: AnyObj, error?: Error];
+```
+
+## Related
+
+- [`createLoggerFactory` API](./create-logger-factory.md)
+- [Formatting](../formatting.md)
+- [Metadata](../metadata.md)
diff --git a/apps/docs/log/building-your-own-logger.md b/apps/docs/log/building-your-own-logger.md
new file mode 100644
index 0000000..560d985
--- /dev/null
+++ b/apps/docs/log/building-your-own-logger.md
@@ -0,0 +1,120 @@
+# Creating your own logger
+
+We're providing built-in [createLogger](./api/create-logger.md) API with good defaults, but you can create your own logger factory
+to provide your own shared logger factory with your own defaults and specific parts of implementation.
+
+To create your own logger factory you need to use [createLoggerFactory](./api/create-logger-factory.md) API.
+
+## Default logger factory
+
+```typescript
+import { createLoggerFactory, DEFAULT_LOGGER_PARAMS } from '@neodx/log';
+import { printf, readArguments } from '@neodx/log/utils';
+
+/**
+ * This is default logger factory exported by `@neodx/log` and `@neodx/log/node`.
+ * `@neodx/log/node` adds `pretty (dev)` and `json (prod)` transports to this factory.
+ * `@neodx/log` adds `console` target in browser environment (in node environment it's replaced by `@neodx/log/node`).
+ */
+export const createLogger = createLoggerFactory({
+ defaultParams: {
+ ...DEFAULT_LOGGER_PARAMS
+ },
+ readArguments,
+ formatMessage: printf
+});
+```
+
+## Override it
+
+You can see details about default levels at [`createLogger` API Reference](./api/create-logger.md#defaultlevel).
+
+```typescript
+import { createLoggerFactory, DEFAULT_LOGGER_LEVELS } from '@neodx/log';
+import { pretty, json } from '@neodx/log/node';
+import { printf, readArguments } from '@neodx/log/utils';
+
+/**
+ * This is default logger factory exported by `@neodx/log` and `@neodx/log/node`.
+ * `@neodx/log/node` adds `pretty (dev)` and `json (prod)` transports to this factory.
+ * `@neodx/log` adds `console` target in browser environment (in node environment it's replaced by `@neodx/log/node`).
+ */
+export const createLogger = createLoggerFactory({
+ defaultParams: {
+ levels: {
+ ...DEFAULT_LOGGER_LEVELS,
+ details: 50, // Add new level
+ debug: 60 // Override existing level
+ },
+ level: 'details', // Set default level
+ name: 'my-app',
+ transform: [],
+ target: [
+ process.env.NODE_ENV === 'production'
+ ? json() // stream JSON logs to stdout
+ : // show pretty formatted logs in console
+ pretty({
+ displayMs: true,
+ levelColors: {
+ ...pretty.defaultColors,
+ details: 'magenta' // Add new level color
+ },
+ levelBadges: {
+ ...pretty.defaultBadges,
+ details: 'ðĪŠ' // Add new level badge
+ }
+ })
+ ],
+ meta: {
+ pid: process.pid
+ }
+ },
+ readArguments,
+ formatMessage: printf
+});
+```
+
+## Replace everything
+
+You can provide your own default levels, [formatter](./api/printf.md) and [arguments processor](./api/read-arguments.md):
+
+```typescript
+import { createLoggerFactory, LOGGER_SILENT_LEVEL } from '@neodx/log';
+import { pretty, json } from '@neodx/log/node';
+import { printf, readArguments } from '@neodx/log/utils';
+import { format } from 'node:util';
+
+export const createLogger = createLoggerFactory({
+ defaultParams: {
+ levels: {
+ hello: 10,
+ world: 'hello', // alias
+ debug: 20,
+ [LOGGER_SILENT_LEVEL]: Infinity // special level to disable logging
+ },
+ level: 'world',
+ name: 'my-app',
+ transform: [],
+ target: [
+ /* ... */
+ ],
+ meta: {
+ /* ... */
+ }
+ },
+ readArguments(...args) {
+ // ... your own implementation
+ },
+ formatMessage: (message, replaces) => format(message, ...replaces) // your own formatter
+});
+
+const logger = createLogger();
+
+logger.hello('Hello, %s!', 'world');
+```
+
+## Related
+
+- [createLoggerFactory](./api/create-logger-factory.md)
+- [createLogger](./api/create-logger.md)
+- [Logger](./api/logger.md)
diff --git a/apps/docs/log/child-and-fork.md b/apps/docs/log/child-and-fork.md
deleted file mode 100644
index 75ed80a..0000000
--- a/apps/docs/log/child-and-fork.md
+++ /dev/null
@@ -1,39 +0,0 @@
-# Children and fork
-
-::: danger WIP
-Documentation is under construction...
-:::
-
-## Forking
-
-```typescript
-const logger = createLogger({
- name: 'my-logger'
-});
-
-const fork = logger.fork({
- level: 'debug'
-});
-``;
-```
-
-## Children
-
-```typescript
-const logger = createLogger({
- name: 'my-logger'
-});
-
-const child = logger.child('child-logger');
-```
-
-### Override params
-
-```typescript
-const child = logger.child('child-logger', {
- level: 'debug',
- meta: {
- service: 'my-service'
- }
-});
-```
diff --git a/apps/docs/log/custom.md b/apps/docs/log/custom.md
deleted file mode 100644
index 3f26a71..0000000
--- a/apps/docs/log/custom.md
+++ /dev/null
@@ -1,24 +0,0 @@
-# Creating your own logger
-
-::: danger WIP
-Documentation is under construction...
-:::
-
-```typescript
-import { createLoggerFactory } from '@neodx/log';
-
-const createLogger = createLoggerFactory({
- defaultParams: {
- meta: {
- app: 'my-app'
- },
- level: 'info',
- name: 'kek-logger',
- target: [
- {
- level: 'error'
- }
- ]
- }
-});
-```
diff --git a/apps/docs/log/fork-and-child.md b/apps/docs/log/fork-and-child.md
new file mode 100644
index 0000000..bd3964c
--- /dev/null
+++ b/apps/docs/log/fork-and-child.md
@@ -0,0 +1,54 @@
+# Forked and child loggers
+
+We're providing two ways to create a new logger instance based on the existing one:
+
+- `.fork()` - creates a copy of the logger with the same params as the original one, but with the ability to override them.
+- `.child()` - creates a nested branded logger with optionally overridden params.
+
+## `.fork(params?)`
+
+```typescript
+const logger = createLogger({
+ name: 'my-logger',
+ meta: {
+ // ... some meta
+ },
+ level: 'info'
+});
+
+const fork = logger.fork({
+ level: 'debug'
+});
+
+logger.debug('debug'); // SKIP
+fork.debug('debug'); // [my-logger] debug
+```
+
+## `.child(name, params?)`
+
+Merging name with the parent name:
+
+```typescript
+const logger = createLogger({
+ name: 'foo'
+});
+
+const child = logger.child('bar');
+
+child.info('info'); // [foo:bar] info
+```
+
+### Override params
+
+```typescript
+const child = logger.child('child-logger', {
+ level: 'debug',
+ meta: {
+ service: 'my-service'
+ }
+});
+```
+
+## Related
+
+- [Metadata](./metadata.md)
diff --git a/apps/docs/log/formatting.md b/apps/docs/log/formatting.md
new file mode 100644
index 0000000..66111ce
--- /dev/null
+++ b/apps/docs/log/formatting.md
@@ -0,0 +1,72 @@
+# Formatting
+
+The simplest log message is just a string, but, of course, you always need to add some additional data to it.
+
+Usually we're solving it with template strings:
+
+```typescript
+`message ${arg1} ${arg2}`;
+```
+
+But it's not very convenient; especially when you have a lot of arguments,
+instead of a clean template string, you'll get a complex unreadable expression.
+
+We're providing a simple [printf](./api/printf.md)-like formatting for log messages:
+
+```typescript
+log.info('Hello %s!', 'world');
+// > Hello world!
+log.info('Value of %s is %d', 'foo', 10);
+// > Value of foo is 10
+log.info(
+ 'Session for "%s" has been closed with %d (details: %j)',
+ '123',
+ SessionCloseReason.Timeout,
+ { reason: 'timeout' }
+);
+// > Session for "123" has been closed with 1000 (details: {"reason":"timeout"})
+```
+
+## Formulae
+
+### `log(template: string, ...args: any[])`
+
+The first argument is a template string, all other arguments will be used to replace placeholders in the template.
+
+```typescript
+log.info('Hello %s!', 'world');
+```
+
+### `log(metadata: object, template?: string, ...args: any[])`
+
+The first argument is a metadata object, second argument is a template string, all other arguments will be used to replace placeholders in the template.
+
+```typescript
+log.info({ foo: 'bar' }, 'Hello %s!', 'world');
+```
+
+### `log(error: Error, template?: string, ...args: any[])`
+
+The first argument is an error object, second argument is a template string, all other arguments will be used to replace placeholders in the template.
+
+```typescript
+log.error(new Error('Something went wrong'));
+// or
+log.error(new Error('Something went wrong'), 'Hello %s!', 'world');
+```
+
+### `log({ err, ...metadata }, template?: string, ...args: any[])`
+
+Error with additional metadata.
+
+The first argument is an object with `err` property, second argument is a template string, all other arguments will be used to replace placeholders in the template.
+
+```typescript
+log.error({ err: new Error('Something went wrong') });
+// or
+log.error({ err: new Error('Something went wrong'), foo: 'bar' }, 'Hello %s!', 'world');
+```
+
+## Related
+
+- [Metadata](./metadata.md)
diff --git a/apps/docs/log/frameworks/express.md b/apps/docs/log/frameworks/express.md
new file mode 100644
index 0000000..c745a7b
--- /dev/null
+++ b/apps/docs/log/frameworks/express.md
@@ -0,0 +1,42 @@
+# Add `@neodx/log` to [Express](https://expressjs.com/) app
+
+
+
+::: info
+In Express we can't handle errors in the single middleware, so we need to use additional `preserveErrorMiddleware` middleware to handle errors.
+:::
+
+## Getting started
+
+To integrate `@neodx/log` with Express app, you need to create a special middleware from `@neodx/log/express` module
+and register it in the top of the middleware stack.
+
+```typescript
+import { createExpressLogger } from '@neodx/log/express';
+import { createLogger } from '@neodx/log/node';
+import express from 'express';
+import createError from 'http-errors';
+
+const app = express();
+const expressLogger = createExpressLogger(); // [!code hl]
+
+app.use(expressLogger); // [!code hl]
+app.get('/', (req, res) => {
+ res.send('respond with a resource');
+});
+app.get('/:id', (req, res) => {
+ req.log.info('Requested user ID %s', req.params.id);
+ res.status(200).json({ id: req.params.id });
+});
+// ... other routes
+app.use((req, res, next) => {
+ next(createError(404));
+});
+// We need to use this middleware to handle errors
+app.use(expressLogger.preserveErrorMiddleware); // [!code hl]
+```
+
+## Related
+
+- [Node.js http adapter](./http.md)
+- [`@neodx/log/http` API](../api/http.md)
diff --git a/apps/docs/log/frameworks/http.md b/apps/docs/log/frameworks/http.md
new file mode 100644
index 0000000..b913acd
--- /dev/null
+++ b/apps/docs/log/frameworks/http.md
@@ -0,0 +1,51 @@
+# Add `@neodx/log` to [Node.js HTTP Server](https://nodejs.org/api/http.html)
+
+
+
+`@neodx/log/http` is a core module for logging HTTP requests and responses.
+It is used by the [`@neodx/log/express`](./express.md) and [`@neodx/log/koa`](./koa.md) adapters.
+
+## Getting Started
+
+`node:http` is low-level module, you need to write all execution logic by yourself.
+
+To log http requests and responses, you need to pass `req` and `res` objects to our http logger handler:
+
+```typescript
+import { createHttpLogger } from '@neodx/log/http';
+import { createLogger } from '@neodx/log/node';
+import createError from 'http-errors';
+import { createServer } from 'node:http';
+
+const dev = process.env.NODE_ENV !== 'production';
+const port = process.env.PORT || 3000;
+
+const httpLogger = createHttpLogger(); // [!code hl]
+const server = createServer((req, res) => {
+ httpLogger(req, res); // [!code hl]
+ if (req.url === '/users') {
+ res.setHeader('Content-Type', 'application/json');
+ res.writeHead(200);
+ res.end(
+ JSON.stringify([
+ { id: 1, name: 'John' },
+ { id: 2, name: 'Jane' }
+ ])
+ );
+ } else {
+ res.err = createError(404);
+ res.writeHead(404);
+ res.end('Unknown route');
+ }
+});
+
+server.listen(port, () => {
+ logger.success(`Example app listening on port ${port}!`);
+});
+```
+
+## Related
+
+- [`@neodx/log/express` adapter](./express.md)
+- [`@neodx/log/koa` adapter](./koa.md)
+- [`@neodx/log/http` API](../api/http.md)
diff --git a/apps/docs/log/frameworks/index.md b/apps/docs/log/frameworks/index.md
new file mode 100644
index 0000000..6fce610
--- /dev/null
+++ b/apps/docs/log/frameworks/index.md
@@ -0,0 +1,13 @@
+# HTTP frameworks
+
+::: tip
+You can view our simple [Framework logging showcase example](https://github.com/secundant/neodx/tree/main/examples/log-frameworks-showcase) to see actual usage of the following examples.
+:::
+
+At the current moment, we support the following HTTP frameworks:
+
+- [Express](./express.md)
+- [Koa](./koa.md)
+- [Node HTTP](./http.md)
+
+All adapters are based on the [Node adapter](#node-http) and have the same API.
diff --git a/apps/docs/log/frameworks/koa.md b/apps/docs/log/frameworks/koa.md
new file mode 100644
index 0000000..606af7e
--- /dev/null
+++ b/apps/docs/log/frameworks/koa.md
@@ -0,0 +1,45 @@
+# Add `@neodx/log` to [Koa](https://koajs.com) app
+
+
+
+## Getting started
+
+```typescript
+import { createKoaLogger } from '@neodx/log/koa';
+import { createLogger } from '@neodx/log/node';
+import createError from 'http-errors';
+import Koa from 'koa';
+
+const dev = process.env.NODE_ENV !== 'production';
+const port = process.env.PORT || 3000;
+
+const app = new Koa();
+const koaLogger = createKoaLogger(); // [!code hl]
+
+app.use(koaLogger); // [!code hl]
+app.use(async (ctx, next) => {
+ await next();
+ const status = ctx.status || 404;
+
+ if (status === 404) {
+ ctx.throw(createError(404));
+ }
+});
+app.get('/users', ctx => {
+ ctx.body = 'respond with a resource';
+});
+app.get('/users/:id', ctx => {
+ ctx.req.log.info('Requested user ID %s', ctx.params.id);
+ ctx.status = 200;
+ ctx.body = { id: ctx.params.id };
+});
+
+app.listen(port, () => {
+ logger.success(`Example app listening on port ${port}!`);
+});
+```
+
+## Related
+
+- [Node.js http adapter](./http.md)
+- [`@neodx/log/http` API](../api/http.md)
diff --git a/apps/docs/log/http-frameworks.md b/apps/docs/log/http-frameworks.md
deleted file mode 100644
index 73c5005..0000000
--- a/apps/docs/log/http-frameworks.md
+++ /dev/null
@@ -1,115 +0,0 @@
-# HTTP frameworks
-
-::: tip
-You can view our simple [Framework logging showcase example](https://github.com/secundant/neodx/examples/log-frameworks-showcase) to see actual usage of the following examples.
-:::
-
-At the current moment, we support the following HTTP frameworks:
-
-- [Express](#express)
-- [Koa](#koa)
-- [Node HTTP](#node-http)
-
-All adapters are based on the [Node adapter](#node-http) and have the same API.
-
-## Express
-
-::: info
-In Express we can't handle errors in the single middleware, so we need to use additional `preserveErrorMiddleware` middleware to handle errors.
-:::
-
-```typescript
-import { createExpressLogger } from '@neodx/log/express';
-import { createLogger } from '@neodx/log/node';
-import express from 'express';
-import createError from 'http-errors';
-
-const app = express();
-const expressLogger = createExpressLogger(); // [!code hl]
-
-app.use(expressLogger); // [!code hl]
-app.get('/', (req, res) => {
- res.send('respond with a resource');
-});
-app.get('/:id', (req, res) => {
- req.log.info('Requested user ID %s', req.params.id);
- res.status(200).json({ id: req.params.id });
-});
-// ... other routes
-app.use((req, res, next) => {
- next(createError(404));
-});
-app.use(expressLogger.preserveErrorMiddleware); // [!code hl]
-```
-
-## Koa
-
-```typescript
-import { createKoaLogger } from '@neodx/log/koa';
-import { createLogger } from '@neodx/log/node';
-import createError from 'http-errors';
-import Koa from 'koa';
-
-const dev = process.env.NODE_ENV !== 'production';
-const port = process.env.PORT || 3000;
-
-const app = new Koa();
-const koaLogger = createKoaLogger(); // [!code hl]
-
-app.use(koaLogger); // [!code hl]
-app.use(async (ctx, next) => {
- await next();
- const status = ctx.status || 404;
-
- if (status === 404) {
- ctx.throw(createError(404));
- }
-});
-app.get('/users', ctx => {
- ctx.body = 'respond with a resource';
-});
-app.get('/users/:id', ctx => {
- ctx.req.log.info('Requested user ID %s', ctx.params.id);
- ctx.status = 200;
- ctx.body = { id: ctx.params.id };
-});
-
-app.listen(port, () => {
- logger.success(`Example app listening on port ${port}!`);
-});
-```
-
-## Node HTTP
-
-```typescript
-import { createHttpLogger } from '@neodx/log/http';
-import { createLogger } from '@neodx/log/node';
-import createError from 'http-errors';
-import { createServer } from 'node:http';
-
-const dev = process.env.NODE_ENV !== 'production';
-const port = process.env.PORT || 3000;
-
-const httpLogger = createHttpLogger(); // [!code hl]
-const server = createServer((req, res) => {
- httpLogger(req, res); // [!code hl]
- if (req.url === '/users') {
- res.setHeader('Content-Type', 'application/json');
- res.writeHead(200);
- res.end(
- JSON.stringify([
- { id: 1, name: 'John' },
- { id: 2, name: 'Jane' }
- ])
- );
- } else {
- res.err = createError(404);
- res.writeHead(404);
- res.end('Unknown route');
- }
-});
-
-server.listen(port, () => {
- logger.success(`Example app listening on port ${port}!`);
-});
-```
diff --git a/apps/docs/log/index.md b/apps/docs/log/index.md
index 7ee2bdd..19a09fa 100644
--- a/apps/docs/log/index.md
+++ b/apps/docs/log/index.md
@@ -1,57 +1,110 @@
# @neodx/log
-::: danger WIP
-Documentation is under construction...
-:::
-
-Powerful lightweight logger for new level of experience.
+Powerful lightweight logger for any requirements.
-![preview](./preview-intro.png)
+
-- **Tiny and simple**. `< 1kb!` without extra configuration
-- **Fast enough**. No extra overhead, no hidden magic
-- **Customizable**. You can replace most of the parts with your own
-- **Isomorphic**. Automatically works in Node.js and browsers
-- **Typed**. Written in TypeScript, with full type support
-- **Well featured**. JSON logs, pretty console logs, error handling, and more
-- **Built-in HTTP frameworks** âïļ`express`, `koa`, Node core `http` loggers are supported out of the box
+- ðĪ **Tiny and simple**. [`< 1kb!`](https://bundlejs.com/?q=%40neodx%2Flog&treeshake=%5B%7B+createLogger+%7D%5D) in browser, no extra configuration
+- ð **Fast enough**. No extra overhead, no hidden magic
+- ðïļ **Customizable**. You can replace core logic and [build your own logger](./building-your-own-logger.md) from scratch
+- ð **Rich features and DX**. [JSON logs](./targets/json.md), [pretty dev logs](./targets/pretty.md), readable errors, and more
+- ð **Well typed**. Written in TypeScript, with full type support
+- ðŦĒ **Built-in HTTP frameworks** âïļ [express](./frameworks/express.md), [koa](./frameworks/koa.md), [Node core `http`](./frameworks/http.md) loggers are supported out of the box
+- ð **Isomorphic**. Automatically works in Node.js and browsers
## Installation
::: code-group
-```bash [Yarn]
-yard add @neodx/log
+```bash [npm]
+npm install -D @neodx/log
```
-```bash [NPM]
-npm install @neodx/log
+```bash [yarn]
+yarn add -D @neodx/log
```
-```bash [PNPM]
-pnpm add @neodx/log
+```bash [pnpm]
+pnpm add -D @neodx/log
```
:::
## Getting Started
-Let's start from the simplest example:
+To begin using `@neodx/log` easily, you need to create a logger instance using the `createLogger` function.
```ts
+import { createLogger } from '@neodx/log/node';
+
const log = createLogger();
-log.info('Hello, world!'); // [my-app] Hello, world!
+log.error(new Error('Something went wrong')); // Something went wrong
+log.warn('Be careful!'); // Be careful!
+
+log.info('Hello, world!'); // Hello, world!
log.info({ object: 'property' }, 'Template %s', 'string'); // Template string { object: 'property' }
+
+log.done('Task completed'); // Task completed
+log.success('Alias for done');
+
log.debug('Some additional information...'); // nothing, because debug level is disabled
+log.verbose('Alias for debug');
+```
-const childLog = log.child('example');
-const needToGoDeeper = childLog.child('next one', {
- level: 'debug'
+### Add child loggers
+
+```typescript
+const child = log.child('child name');
+
+child.info('message'); // [child name] message
+```
+
+### Configure log levels
+
+```typescript
+const log = createLogger({
+ level: process.env.NODE_ENV === 'production' ? 'info' : 'debug'
});
-childLog.warn('Hello, world!'); // [example] Hello, world!
-needToGoDeeper.debug('debug is enabled here'); // [example âš next one] debug is enabled here
+log.success('This message will be logged only in development');
+```
+
+### See detailed errors in development
+
+
+
+Explore [pretty target](./targets/pretty.md) for details.
+
+### Use JSON logs in production
+
+> By default, `@neodx/log` already uses pretty logs in development and JSON logs in production for Node.js environment.
+
+```typescript
+import { createLogger, json, pretty } from '@neodx/log/node';
+
+const log = createLogger({
+ target: process.env.NODE_ENV === 'production' ? json() : pretty()
+});
```
-Here we created a default logger, tried to log some messages, and created a child logger with a different level.
+## Integrate with your framework
+
+We're supporting [built-in integrations](./frameworks/) with a some popular HTTP frameworks:
+
+- [Koa](./frameworks/koa.md)
+- [Express](./frameworks/express.md)
+- [Raw Node.js HTTP module](./frameworks/http.md)
+
+Just add the logger middleware to your app and you're ready to go! For example, for Koa:
+
+```typescript
+import { createKoaLogger } from '@neodx/log/koa';
+// ...
+app.use(createKoaLogger());
+// ...
+function myMiddleware(ctx) {
+ ctx.log.info('Some log message');
+ // ...
+}
+```
diff --git a/apps/docs/log/json.md b/apps/docs/log/json.md
deleted file mode 100644
index 3779309..0000000
--- a/apps/docs/log/json.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# JSON logging
-
-::: danger WIP
-Documentation is under construction...
-:::
diff --git a/apps/docs/log/metadata.md b/apps/docs/log/metadata.md
new file mode 100644
index 0000000..e7897ff
--- /dev/null
+++ b/apps/docs/log/metadata.md
@@ -0,0 +1,50 @@
+# Metadata
+
+Every log message could contain some additional context for future working with.
+
+With [pretty format](./targets/pretty.md) (in development) metadata will be shown as a serialized object at the end of the log with it.
+
+With [JSON logs format](./targets/json.md) metadata fields will be added to the displaying JSON object.
+
+```typescript
+import { createLogger } from '@neodx/log';
+
+const log = createLogger();
+
+// message argument { foo: 'bar' }
+log.info({ foo: 'bar' }, 'message %s', 'argument');
+```
+
+If you want to log error with additional metadata, you should pass error in `{ err: ... }` field, all other fields will be metadata:
+
+```typescript
+// before
+log.error(myError);
+// after
+log.error({ err: myError, foo: 'bar' });
+```
+
+Also, you can provide default metatada:
+
+```typescript
+import { createLogger } from '@neodx/log';
+
+const log = createLogger({
+ meta: {
+ shared: 10
+ }
+});
+
+// message { shared: 10 }
+log.info('message');
+// message { shared: 10, foo: 'bar' }
+log.info({ foo: 'bar' }, 'message');
+// message { shared: 20, foo: 'bar' }
+log.info({ shared: 20, foo: 'bar' }, 'message');
+```
+
+## Related
+
+- [Child and fork](./fork-and-child.md)
+- [Pretty format](./targets/pretty.md)
+- [JSON logs](./targets/json.md)
diff --git a/apps/docs/log/motivation.md b/apps/docs/log/motivation.md
new file mode 100644
index 0000000..32e2685
--- /dev/null
+++ b/apps/docs/log/motivation.md
@@ -0,0 +1,39 @@
+# Motivation
+
+Logging is one of the key aspects of software development, and you've probably heard advice like "Just log everything."
+It's a solid recommendation, and chances are, you agree with it too.
+However, in web development, logging can sometimes become challenging to manage and maintain, leading to a frustrating development experience (DX).
+
+Often, we find ourselves avoiding logs until it becomes inevitable, removing them, or wrapping them in numerous conditions.
+In today's development landscape, logs can be perceived as a hindrance to DX.
+Nevertheless, embracing comprehensive logging is essential for effective software development and is required for building stable products.
+
+## So, what's the problem? Why do we avoid logging?
+
+During software development, developers frequently face the same issue: "How can I turn off, replace, or modify my logs?"
+The inability to easily control logging behavior often leads us to one of two choicesâeither drop the logs (we even have ESLint rules for this purpose) altogether or introduce an abstraction layer.
+
+Dropping logs means avoiding any use of `console.log` and similar APIs, simply because we **cannot control them**.
+
+On the other hand, abstractions come with their own set of trade-offs and there is no widely-accepted, easy-to-use solution available.
+
+## What's the solution?
+
+In my opinion, - we just don't have a good enough abstraction layer for logging:
+
+- Small size
+- Isomorphic
+- Configurable
+- Multiple transports support
+- Multiple log levels support
+- Built-in [pretty-printing](./targets/pretty.md), [JSON logging](./targets/json.md)
+- etc.
+
+Okay, maybe we have some good enough solutions, but they are not perfect:
+
+- [pino](https://www.npmjs.com/package/pino) - **really fast**, but 3kb (browser) size, huge API, no built-in pretty-printing
+- [signale](https://www.npmjs.com/package/signale) - Not maintained, only Node.JS, only pretty-printing, no JSON logging
+- [loglevel](https://www.npmjs.com/package/loglevel) - Not maintained, just a console wrapper
+- Other solutions are not worth mentioning, they are too far from the "just take it and use it" state
+
+And after all, I decided to create my own solution.
diff --git a/apps/docs/log/pretty-printing.md b/apps/docs/log/pretty-printing.md
deleted file mode 100644
index 8b631dc..0000000
--- a/apps/docs/log/pretty-printing.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# Pretty printing
-
-::: danger WIP
-Documentation is under construction...
-:::
-
-```typescript
-import { createLogger, pretty } from '@neodx/log/node';
-
-const logger = createLogger({
- target: pretty()
-});
-```
-
-## Pretty errors
-
-```typescript
-import { createLogger, pretty } from '@neodx/log/node';
-
-const logger = createLogger({
- target: pretty({
- prettyErrors: true
- })
-});
-```
-
-### Configuring pretty errors
-
-```typescript
-import { createLogger, pretty } from '@neodx/log/node';
-
-const logger = createLogger({
- target: pretty({
- prettyErrors: {
- fullStack: true
- }
- })
-});
-```
-
-## Badges
-
-## Levels
-
-## Configuring output
diff --git a/apps/docs/log/preview-intro.png b/apps/docs/log/preview-intro.png
deleted file mode 100644
index 23bebba..0000000
Binary files a/apps/docs/log/preview-intro.png and /dev/null differ
diff --git a/apps/docs/log/targets/json.md b/apps/docs/log/targets/json.md
new file mode 100644
index 0000000..23123ab
--- /dev/null
+++ b/apps/docs/log/targets/json.md
@@ -0,0 +1,59 @@
+# JSON logs
+
+JSON logs are a simple way to log structured data for further processing.
+
+By default, `@neodx/log` uses [Pretty logs](./pretty.md) for development and JSON logs for production.
+
+If you want to reproduce this behavior, you can use the following code:
+
+```ts
+import { createLogger, json, pretty } from '@neodx/log/node';
+
+const logger = createLogger({
+ target: process.env.NODE_ENV === 'production' ? json() : pretty()
+});
+
+logger.info('Hello World!');
+```
+
+The output will be:
+
+```text
+{"level": 30,"time": 1696791460010,"msg": "Hello World!","pid": 87438,"hostname": "my-hostname"}
+```
+
+## Levels
+
+Log levels will be converted to their numeric value.
+
+```ts
+logger.error('error');
+logger.warn('warn');
+logger.info('info');
+logger.done('done');
+logger.debug('debug');
+```
+
+```text
+{"level": 10,"time": 1696791460010,"msg": "error","pid": 87438,"hostname": "my-hostname"}
+{"level": 20,"time": 1696791460010,"msg": "warn","pid": 87438,"hostname": "my-hostname"}
+{"level": 30,"time": 1696791460010,"msg": "info","pid": 87438,"hostname": "my-hostname"}
+{"level": 40,"time": 1696791460010,"msg": "done","pid": 87438,"hostname": "my-hostname"}
+{"level": 50,"time": 1696791460010,"msg": "debug","pid": 87438,"hostname": "my-hostname"}
+```
+
+## Extend JSON
+
+Any additional metadata will be added to the JSON output.
+
+```ts
+logger.info({ foo: 'bar' }, 'Hello World!');
+```
+
+```text
+{"level": 30,"time": 1696791460010,"msg": "Hello World!","pid": 87438,"hostname": "my-hostname","foo": "bar"}
+```
+
+## Related
+
+- [Pretty logs](./pretty.md)
diff --git a/apps/docs/log/targets/pretty.md b/apps/docs/log/targets/pretty.md
new file mode 100644
index 0000000..bb184a1
--- /dev/null
+++ b/apps/docs/log/targets/pretty.md
@@ -0,0 +1,67 @@
+---
+outline: [2, 3]
+---
+
+# Pretty logs
+
+Pretty human-friendly logs output for development! ð
+
+
+
+If you're using pur defaults, `pretty` target will be enabled when `NODE_ENV` is **NOT** `production`.
+
+If you want to enable it manually, you could do it like this:
+
+```typescript
+import { createLogger, pretty } from '@neodx/log/node';
+
+const logger = createLogger({
+ target: [
+ // ...
+ process.env.NODE_ENV !== 'production' && pretty()
+ ]
+});
+```
+
+## Pretty errors
+
+
+
+Errors readability and understandability is one of well-known problems in Node.js and web development in general.
+Different tools provide different interesting ways to handle it, but in our own code we still don't have a good solution.
+
+We're trying to solve this problem and give you great human-friendly errors out of the box:
+
+- Source code frames! ðĨ
+- `.cause` support
+- Serializing additional error properties
+- Highlighting stack frames
+
+### `.cause` and serializing error properties
+
+
+
+### Can we disable it?
+
+As we said, `prettyErrors` is enabled by default, but you could always disable or configure it.
+
+Let's see an output with **disabled** pretty errors option.
+
+```typescript {5}
+import { createLogger, pretty } from '@neodx/log/node';
+
+const logger = createLogger({
+ target: pretty({
+ prettyErrors: false
+ })
+});
+```
+
+So, collapsed, unreadable, and not very useful. Nothing new ð
+
+
+
+## Related
+
+- [`pretty` target API](../api/pretty.md)
+- [JSON logs](./json.md)
diff --git a/apps/docs/package.json b/apps/docs/package.json
index 48b20cb..ca99621 100644
--- a/apps/docs/package.json
+++ b/apps/docs/package.json
@@ -1,9 +1,10 @@
{
"name": "@neodx/docs",
"private": true,
+ "type": "module",
"packageManager": "yarn@3.2.0",
"devDependencies": {
- "vitepress": "^1.0.0-beta.5"
+ "vitepress": "1.0.0-rc.20"
},
"scripts": {
"dev": "vitepress dev",
diff --git a/apps/docs/public/android-chrome-144x144.png b/apps/docs/public/android-chrome-144x144.png
new file mode 100644
index 0000000..b067373
Binary files /dev/null and b/apps/docs/public/android-chrome-144x144.png differ
diff --git a/apps/docs/public/android-chrome-192x192.png b/apps/docs/public/android-chrome-192x192.png
new file mode 100644
index 0000000..ade8459
Binary files /dev/null and b/apps/docs/public/android-chrome-192x192.png differ
diff --git a/apps/docs/public/apple-touch-icon.png b/apps/docs/public/apple-touch-icon.png
new file mode 100644
index 0000000..7c7cf9f
Binary files /dev/null and b/apps/docs/public/apple-touch-icon.png differ
diff --git a/apps/docs/public/browserconfig.xml b/apps/docs/public/browserconfig.xml
new file mode 100644
index 0000000..5aecc91
--- /dev/null
+++ b/apps/docs/public/browserconfig.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+ #00aba9
+
+
+
diff --git a/apps/docs/public/crazy-svg-mix.png b/apps/docs/public/crazy-svg-mix.png
new file mode 100644
index 0000000..68baa0d
Binary files /dev/null and b/apps/docs/public/crazy-svg-mix.png differ
diff --git a/apps/docs/public/favicon-16x16.png b/apps/docs/public/favicon-16x16.png
new file mode 100644
index 0000000..66b7a52
Binary files /dev/null and b/apps/docs/public/favicon-16x16.png differ
diff --git a/apps/docs/public/favicon-32x32.png b/apps/docs/public/favicon-32x32.png
new file mode 100644
index 0000000..0072f35
Binary files /dev/null and b/apps/docs/public/favicon-32x32.png differ
diff --git a/apps/docs/public/favicon.ico b/apps/docs/public/favicon.ico
new file mode 100644
index 0000000..707ca94
Binary files /dev/null and b/apps/docs/public/favicon.ico differ
diff --git a/apps/docs/public/log/example-express-logs.png b/apps/docs/public/log/example-express-logs.png
new file mode 100644
index 0000000..553b134
Binary files /dev/null and b/apps/docs/public/log/example-express-logs.png differ
diff --git a/apps/docs/public/log/example-http-logs.png b/apps/docs/public/log/example-http-logs.png
new file mode 100644
index 0000000..3f5e841
Binary files /dev/null and b/apps/docs/public/log/example-http-logs.png differ
diff --git a/apps/docs/public/log/example-koa-logs.png b/apps/docs/public/log/example-koa-logs.png
new file mode 100644
index 0000000..58714e6
Binary files /dev/null and b/apps/docs/public/log/example-koa-logs.png differ
diff --git a/apps/docs/public/log/pretty-errors-cause.png b/apps/docs/public/log/pretty-errors-cause.png
new file mode 100644
index 0000000..6314f20
Binary files /dev/null and b/apps/docs/public/log/pretty-errors-cause.png differ
diff --git a/apps/docs/public/log/pretty-errors-chained.png b/apps/docs/public/log/pretty-errors-chained.png
new file mode 100644
index 0000000..b45a7d9
Binary files /dev/null and b/apps/docs/public/log/pretty-errors-chained.png differ
diff --git a/apps/docs/public/log/pretty-errors-off.png b/apps/docs/public/log/pretty-errors-off.png
new file mode 100644
index 0000000..506509e
Binary files /dev/null and b/apps/docs/public/log/pretty-errors-off.png differ
diff --git a/apps/docs/public/log/pretty-target.png b/apps/docs/public/log/pretty-target.png
new file mode 100644
index 0000000..6c76fc4
Binary files /dev/null and b/apps/docs/public/log/pretty-target.png differ
diff --git a/apps/docs/public/log/preview-intro.png b/apps/docs/public/log/preview-intro.png
new file mode 100644
index 0000000..8ea8c5e
Binary files /dev/null and b/apps/docs/public/log/preview-intro.png differ
diff --git a/apps/docs/public/logo.png b/apps/docs/public/logo.png
new file mode 100644
index 0000000..b3d7161
Binary files /dev/null and b/apps/docs/public/logo.png differ
diff --git a/apps/docs/public/mstile-150x150.png b/apps/docs/public/mstile-150x150.png
new file mode 100644
index 0000000..b7dec14
Binary files /dev/null and b/apps/docs/public/mstile-150x150.png differ
diff --git a/apps/docs/public/safari-pinned-tab.svg b/apps/docs/public/safari-pinned-tab.svg
new file mode 100644
index 0000000..307a8ef
--- /dev/null
+++ b/apps/docs/public/safari-pinned-tab.svg
@@ -0,0 +1,40 @@
+
+
+
diff --git a/apps/docs/public/site.webmanifest b/apps/docs/public/site.webmanifest
new file mode 100644
index 0000000..b1485ef
--- /dev/null
+++ b/apps/docs/public/site.webmanifest
@@ -0,0 +1,14 @@
+{
+ "name": "Neodx",
+ "short_name": "Neodx",
+ "icons": [
+ {
+ "src": "/android-chrome-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ }
+ ],
+ "theme_color": "#ffffff",
+ "background_color": "#ffffff",
+ "display": "standalone"
+}
diff --git a/apps/docs/public/wrong-svg-size.png b/apps/docs/public/wrong-svg-size.png
new file mode 100644
index 0000000..64fd44d
Binary files /dev/null and b/apps/docs/public/wrong-svg-size.png differ
diff --git a/apps/docs/svg/api/build-sprites.md b/apps/docs/svg/api/build-sprites.md
new file mode 100644
index 0000000..c439b80
--- /dev/null
+++ b/apps/docs/svg/api/build-sprites.md
@@ -0,0 +1,35 @@
+# `buildSprites`
+
+Top-level function to build sprites without any manual actions.
+
+```typescript
+declare function buildSprites(params: BuildSpritesParams): Promise;
+```
+
+## Usage
+
+```typescript
+await buildSprites({
+ root: 'assets/icons',
+ input: '**/*.svg',
+ output: 'public/sprites'
+});
+```
+
+## `BuildSpritesParams`
+
+- [`CreateSpriteBuilderParams`](./create-sprites-builder.md#createspritebuilderparams)
+
+```typescript
+export interface BuildSpritesParams extends CreateSpriteBuilderParams {
+ /**
+ * Globs to icons files
+ */
+ input: string | string[];
+ /**
+ * Keep tree changes after generation even if dry-run mode is enabled
+ * Useful for testing (for example, to check what EXACTLY was changed)
+ */
+ keepTreeChanges?: boolean;
+}
+```
diff --git a/apps/docs/svg/api/create-sprites-builder.md b/apps/docs/svg/api/create-sprites-builder.md
new file mode 100644
index 0000000..ea96d39
--- /dev/null
+++ b/apps/docs/svg/api/create-sprites-builder.md
@@ -0,0 +1,112 @@
+# `createSpritesBuilder`
+
+Creates dynamic sprites builder that can be used in any build flow.
+
+## Reference
+
+- [CreateSpriteBuilderParams](#createspritebuilderparams)
+- [SpriteBuilder](#SpriteBuilder)
+
+```typescript
+declare function createSpritesBuilder(params?: CreateSpriteBuilderParams): SpriteBuilder;
+```
+
+### Usage
+
+```typescript
+const builder = createSpritesBuilder({
+ // ...
+});
+
+await builder.load(['src/icons/*.svg']);
+await builder.build();
+```
+
+## `SpriteBuilder`
+
+```typescript
+export interface SpriteBuilder {
+ /**
+ * Adds new source svg files.
+ * Could be used for
+ */
+ add(paths: string[]): Promise;
+ /**
+ * Add source files by glob pattern(s)
+ */
+ load(patterns: string | string[]): Promise;
+ /**
+ * Removes previously added svg files
+ */
+ remove(paths: string[]): void;
+ /**
+ * Builds all sprites
+ */
+ build(): Promise;
+}
+```
+
+## `CreateSpriteBuilderParams`
+
+- `vfs`: [@neodx/vfs VFS](/vfs/) instance
+- `logger`: [@neodx/log Logger](/log/) or any compatible object
+- `resetColors` - see `resetColors` plugin params at [ResetColorsPluginParams](./plugins/reset-colors.md)
+- `metadata` - see `metadata` plugin params at [MetadataPluginParams](./plugins/metadata.md)
+- `optimize` - see `svgo` plugin params at [SvgoPluginParams](./plugins/svgo.md)
+
+```typescript
+interface Options {
+ /**
+ * VFS instance
+ * @see `@neodx/vfs`
+ * @default createVfs(process.cwd())
+ */
+ vfs?: VFS;
+ /**
+ * Root folder for inputs, useful for correct groups naming
+ * @default process.cwd()
+ */
+ root?: string;
+ /**
+ * Path to generated sprite/sprites folder
+ * @default public
+ */
+ output?: string;
+ /**
+ * Logger instance (or object with any compatible interface)
+ * @see `@neodx/log`
+ * @default built-in logger
+ */
+ logger?: LoggerMethods<'info' | 'debug' | 'error'>;
+ /**
+ * Should we group icons?
+ * @default false
+ */
+ group?: boolean;
+ /**
+ * Template of sprite file name
+ * @example {name}.svg
+ * @example sprite-{name}.svg
+ * @example {name}-{hash}.svg
+ * @example {name}-{hash:8}.svg
+ * @default {name}.svg
+ */
+ fileName?: string;
+ /**
+ * Should we optimize icons?
+ */
+ optimize?: false | SvgoPluginParams;
+ /**
+ * Configures metadata generation
+ * @example "src/sprites/meta.ts"
+ * @example { path: "meta.ts", runtime: false } // will generate only types
+ * @example { path: "meta.ts", types: 'TypeName', runtime: 'InfoName' } // will generate "interface TypeName" types and "const InfoName" runtime metadata
+ * @example { path: "meta.ts", runtime: { size: true, viewBox: true } } // will generate runtime metadata with size and viewBox
+ */
+ metadata?: MetadataPluginParams;
+ /**
+ * Reset colors config
+ */
+ resetColors?: ResetColorsPluginParams;
+}
+```
diff --git a/apps/docs/svg/api/create-watcher.md b/apps/docs/svg/api/create-watcher.md
new file mode 100644
index 0000000..1ffaf9d
--- /dev/null
+++ b/apps/docs/svg/api/create-watcher.md
@@ -0,0 +1,44 @@
+# `createWatcher`
+
+Create a [chokidar watcher](https://www.npmjs.com/package/chokidar) that is integrated with [SpriteBuilder](./create-sprites-builder.md).
+This watcher will monitor the source paths for additions, changes, and removals, and it will trigger a rebuild of the sprites when necessary.
+
+```typescript
+import type { FSWatcher } from 'chokidar';
+
+declare function createWatcher({ root = '.', input, builder }: CreateWatcherParams): FSWatcher;
+```
+
+## Usage
+
+```typescript
+const root = 'assets/icons';
+const input = '**/*.svg';
+
+const builder = createSpritesBuilder({
+ root,
+ input,
+ output: 'public/sprites'
+});
+const watcher = createWatcher({
+ root,
+ input,
+ builder
+});
+
+await builder.load(input);
+await builder.build();
+await builder.vfs.applyChanges();
+```
+
+## `CreateWatcherParams`
+
+- [`SpriteBuilder`](./create-sprites-builder.md#spritebuilder)
+
+```typescript
+export interface CreateWatcherParams {
+ builder: SpriteBuilder;
+ root: string;
+ input: string | string[];
+}
+```
diff --git a/apps/docs/svg/api/index.md b/apps/docs/svg/api/index.md
new file mode 100644
index 0000000..c01efea
--- /dev/null
+++ b/apps/docs/svg/api/index.md
@@ -0,0 +1,11 @@
+# `@neodx/svg` API Reference
+
+## Core
+
+- [createSpritesBuilder](./create-sprites-builder.md)
+- [createWatcher](./create-watcher.md)
+- [buildSprites](./build-sprites.md)
+
+## Plugins
+
+- [resetColors](./plugins/reset-colors.md)
diff --git a/apps/docs/svg/api/plugins/metadata.md b/apps/docs/svg/api/plugins/metadata.md
new file mode 100644
index 0000000..40786ea
--- /dev/null
+++ b/apps/docs/svg/api/plugins/metadata.md
@@ -0,0 +1,42 @@
+# `metadata` plugin
+
+Unified plugin for generating runtime and types metadata for sprites.
+
+## `MetadataPluginParams`
+
+```typescript
+/**
+ * false - disable metadata generation
+ * string - path to generated file, alias to { path: string }
+ * MetadataPluginParamsConfig - full configuration
+ */
+export type MetadataPluginParams = false | string | MetadataPluginParamsConfig;
+
+export interface MetadataPluginParamsConfig {
+ path: string;
+ types?: Partial | boolean | string;
+ runtime?: Partial | boolean | string;
+}
+
+export interface MetadataTypesParams {
+ /**
+ * Name of generated interface
+ * @example "SpritesMetadata"
+ * @default "SpritesMap"
+ */
+ name: string;
+}
+
+export interface MetadataRuntimeParams {
+ /**
+ * Name of generated runtime metadata
+ * @example "sprites"
+ * @default "SPRITES_META"
+ */
+ name: string;
+ // Enable/disable width/height generation
+ size?: boolean;
+ // Enable/disable viewBox generation
+ viewBox?: boolean;
+}
+```
diff --git a/apps/docs/svg/api/plugins/reset-colors.md b/apps/docs/svg/api/plugins/reset-colors.md
new file mode 100644
index 0000000..5a0bf83
--- /dev/null
+++ b/apps/docs/svg/api/plugins/reset-colors.md
@@ -0,0 +1,53 @@
+# `resetColors` plugin
+
+`resetColors` plugin allows applying complex color replacement logic to building SVG sprites.
+
+## `ResetColorsPluginParams`
+
+::: tip
+We're using [colord](https://github.com/omgovich/colord) to parse colors, so you can pass any color format supported by it.
+:::
+
+```typescript
+import { type AnyColor, type Colord } from 'colord';
+
+// You can pass single config object or array of them
+export type ResetColorsPluginParams =
+ | ColorPropertyReplacementInput
+ | ColorPropertyReplacementInput[];
+
+interface ColorPropertyReplacementInput {
+ // SVG props to replace colors in (default: 'fill', 'stroke')
+ properties?: string | string[];
+ // Colors to keep untouched
+ keep?: AnyColorInput | AnyColorInput[];
+ // Included files filter. If not specified, all files will be included
+ include?: FileFilterInput;
+ // Excluded files filter. If not specified, no files will be excluded
+ exclude?: FileFilterInput;
+ /**
+ * Manual color replacement config
+ * @example { from: 'red', to: 'currentColor' }
+ * @example { from: ['red', 'green'], to: 'var(--icon-primary-color)' }
+ * @example 'red' // equals to { from: 'red', to: 'currentColor' }
+ * @default [] // no manual replacement
+ */
+ replace?: ColorReplacementInput | ColorReplacementInput[];
+ /**
+ * Color (or any token) to replace unknown colors with
+ * @example 'currentColor'
+ * @example 'var(--icon-primary-color)'
+ */
+ replaceUnknown?: string;
+}
+
+export type AnyColorInput = AnyColor | Colord;
+export type FileFilterInput = FileFilterInputValue | FileFilterInputValue[];
+export type FileFilterInputValue = string | RegExp;
+export type ColorReplacementInput = string | ColorReplacementInputConfig;
+
+export interface ColorReplacementInputConfig {
+ from: AnyColorInput | AnyColorInput[];
+ to?: string;
+}
+```
diff --git a/apps/docs/svg/api/plugins/svgo.md b/apps/docs/svg/api/plugins/svgo.md
new file mode 100644
index 0000000..c5d4154
--- /dev/null
+++ b/apps/docs/svg/api/plugins/svgo.md
@@ -0,0 +1,27 @@
+# `svgo` plugin
+
+Optimize svg files using [svgo](https://github.com/svg/svgo).
+
+## `SvgoPluginParams`
+
+```typescript
+import { type Config } from 'svgo';
+
+export interface SvgoPluginParams {
+ /**
+ * Additional attributes to remove
+ * By default we remove next attributes:
+ * - '(class|style)',
+ * - 'xlink:href',
+ * - 'aria-labelledby',
+ * - 'aria-describedby',
+ * - 'xmlns:xlink',
+ * - 'data-name'
+ */
+ removeAttrs: string[];
+ /**
+ * Override default svgo config
+ */
+ config?: Config;
+}
+```
diff --git a/apps/docs/svg/cli.md b/apps/docs/svg/cli.md
new file mode 100644
index 0000000..ce14451
--- /dev/null
+++ b/apps/docs/svg/cli.md
@@ -0,0 +1,58 @@
+# CLI
+
+Currently, we don't recommend using CLI mode because it's not flexible enough and requires extra setup
+if you want to use it - see [CLI](#cli) section and [CLI Options API](#cli-options).
+
+```shell
+yarn sprite --help
+```
+
+Let's run `sprite` with some additional options:
+
+```bash
+yarn sprite --group --root assets -o public/sprite -d src/shared/ui/icon/sprite.gen.ts --reset-unknown-colors
+```
+
+In details:
+
+- The `--group` option group icons by folders (`common` and `other`)
+- The `--root` option sets `assets` as a base path for icons (you can try to remove it and see the difference)
+- The `-o` option sets `public/sprite` as a base path for generated sprites (it's default value, but let's keep it for now)
+- The `-d` option generates TS definitions file with sprite meta information
+
+## CLI
+
+> **Warning:**
+> While the CLI mode is currently available,
+> it's not the recommended method of use and might be removed in future major versions.
+>
+> Now we're providing built-it bundlers integration, please, use [our plugin](#integrate-with-your-bundler) instead.
+
+To get started, you can try the CLI mode even without any configuration, just run `sprite` command:
+
+```shell
+yarn sprite
+```
+
+This command searches for all SVG files, excluding those in the `public/sprites` folder and generate sprites in `public/sprites`.
+
+By default, it creates a single sprite containing all icons without any grouping or TS definitions. However, this can be customized. See [CLI options](#cli-options) for more information
+
+### CLI Options
+
+| option | default | description |
+| -------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
+| `-i`, `--input` | `"**/*.svg"` | Glob paths to icons files (output path will be automatically excluded) |
+| `-o`, `--output` | `"public/sprites"` | Base path to generated sprite/sprites folder |
+| `-d`, `--definitions` | Not provided (**disabled**) | Path to generated TS file with sprite meta |
+| `--root` | `"."` (same as the current dir) | Base path to your assets, useful for correct groups names **careful:** `--input` should be relative to `--root` |
+| `--group` | `false` | Should we group icons by folders? |
+| `--dry-run` | `false` | Print proposal of generated file paths without actually generating it |
+| `--optimize` | `true` | Should we optimize SVG with [svgo](https://github.com/svg/svgo)? |
+| `--reset-color-values` | `"#000,#000000"` | An array of colors to replace as `currentColor` |
+| `--reset-unknown-colors` | `false` | Should we set `currentColor` for all colors not defined in `--reset-color-values`, or for all colors if this option isn't provided? |
+| `--reset-color-properties` | `"fill,stroke"` | An array of SVG properties that will be replaced with `currentColor` if they're present |
+
+> **Note:** `--reset-color-values` and `--reset-color-properties` are strings with comma-separated values, don't forget to wrap them with quotes:
+>
+> `sprite ... --reset-color-values "#000,#000000,#fff"`
diff --git a/apps/docs/svg/colors-reset.md b/apps/docs/svg/colors-reset.md
index 7fe25d3..274991f 100644
--- a/apps/docs/svg/colors-reset.md
+++ b/apps/docs/svg/colors-reset.md
@@ -1,5 +1,11 @@
+---
+outline: [2, 3]
+---
+
# Automatically reset colors
+A powerful feature to automatically reset colors in SVG icons.
+
```typescript
svg({
resetColors: {
@@ -9,7 +15,7 @@ svg({
});
```
-Automate your icons and forget about colors management issues.
+- [API Reference](./api/plugins/reset-colors.md)
## Motivation
@@ -32,7 +38,9 @@ To solve these issues, we're introducing a `resetColors` option:
- Multiple configurations for different colors strategies
- Granular control with colors and files filters
-## Disable colors reset
+## Usage
+
+### Disable colors reset
```typescript
svg({
@@ -41,7 +49,7 @@ svg({
});
```
-## Filter colors and icons
+### Filter colors and icons
```typescript
svg({
@@ -57,7 +65,7 @@ svg({
});
```
-## Replace specific colors
+### Replace specific colors
> Without `replaceUnknown` option, all unspecified colors will be kept as is.
@@ -86,7 +94,7 @@ svg({
});
```
-## All in one
+### All in one
- Replace white color in all flags with `currentColor`
- For all icons except flags, logos and colored icons:
@@ -125,3 +133,8 @@ svg({
]
});
```
+
+## Related
+
+- ["Working with multicolored" guide](./multicolored.md)
+- ["Writing Icon Component" guide](./writing-icon-component.md)
diff --git a/apps/docs/svg/frameworks-and-bundlers.md b/apps/docs/svg/frameworks-and-bundlers.md
deleted file mode 100644
index 36140b4..0000000
--- a/apps/docs/svg/frameworks-and-bundlers.md
+++ /dev/null
@@ -1,32 +0,0 @@
-# Frameworks and bundlers
-
-::: danger WIP
-Documentation is under construction...
-:::
-
-We're using [unplugin](https://github.com/unjs/unplugin) to minimize additional efforts to integrate with popular bundlers.
-
-## Vite
-
-```typescript
-import svg from '@neodx/svg/vite';
-import react from '@vitejs/plugin-react';
-import { defineConfig } from 'vite';
-import tsconfigPaths from 'vite-tsconfig-paths';
-
-export default defineConfig({
- plugins: [
- react(),
- tsconfigPaths(),
- svg({
- root: 'assets',
- group: true,
- output: 'public',
- metadata: 'src/shared/ui/icon/sprite.gen.ts',
- resetColors: {
- replaceUnknown: 'currentColor'
- }
- })
- ]
-});
-```
diff --git a/apps/docs/svg/group-and-hash.md b/apps/docs/svg/group-and-hash.md
new file mode 100644
index 0000000..e8a458e
--- /dev/null
+++ b/apps/docs/svg/group-and-hash.md
@@ -0,0 +1,88 @@
+# Group and hash sprites
+
+## Missed features of SVG in JS
+
+Despite the all disadvantages of SVG in JS, it has some design features:
+
+- Bundlers will chunk icon components by their usage, so we'll have a lot of on-demand chunks instead of one big sprite.
+- Components will be tree-shaken, so we'll have only used icons in the final bundle.
+- As we're getting JS bundle, we're always seeing up-to-date icons
+
+## Solving problems
+
+To solve these problems at least partially, we're providing next features:
+
+- Grouping icons into multiple sprites by directory name
+- Adding hash to sprite file name to prevent caching issues
+- [Generating metadata](./metadata.md) (width, height, viewBox, and sprite file path) for runtime usage
+
+Imagine that we already have the following sprites in your output with regular configuration:
+
+```diff
+/
+âââ assets
+â âââ common
+â â âââ left.svg
+â â âââ right.svg
+â âââ actions
+â âââ close.svg
+âââ public
++ âââ sprites
++ âââ common.svg
++ âââ actions.svg
+```
+
+But this is not very good for caching, because if you change any of the SVG files,
+the sprite filename won't be updated, which could result in an infinite cache.
+
+To solve this issue and achieve content-based hashes in filenames, you need to take three steps:
+
+1. Provide the `fileName` option with a `hash` variable (e.g. `fileName: "{name}.{hash:8}.svg"`)
+2. Configure the `metadata` option to get additional information about the file path by sprite name during runtime
+3. Update your `Icon` component (or whatever you use) to support the new runtime information
+
+::: code-group
+
+```typescript {9,11} [vite.config.ts]
+import svg from '@neodx/svg/vite';
+
+export default defineConfig({
+ plugins: [
+ svg({
+ root: 'assets',
+ output: 'public/sprites',
+ // group icons by sprite name
+ group: true,
+ // add hash to sprite file name
+ fileName: '{name}.{hash:8}.svg'
+ })
+ ]
+});
+```
+
+:::
+
+Now you will get the following output:
+
+```diff
+/
+âââ assets
+â âââ common
+â â âââ left.svg
+â â âââ right.svg
+â âââ actions
+â âââ close.svg
+âââ public
++ âââ sprites
++ âââ common.12ghS6Uj.svg
++ âââ actions.1A34ks78.svg
+```
+
+In the result, we will solve the following problems:
+
+- Now all icons are grouped into multiple sprites which will be loaded on-demand
+- Sprite file names contain hash to prevent caching issues
+
+But now we don't know actual file names in runtime! ð
+
+Let's close this gap by learning about [metadata](./metadata.md) and [writing well-featured `Icon` component](./writing-icon-component.md)!
diff --git a/apps/docs/svg/index.md b/apps/docs/svg/index.md
index ecbea2c..266e14e 100644
--- a/apps/docs/svg/index.md
+++ b/apps/docs/svg/index.md
@@ -1,14 +1,122 @@
# @neodx/svg
-::: danger WIP
-Documentation is under construction...
+Supercharge your icons âĄïļ
+
+- TypeScript support out of box - [generated types and information about your sprites](./metadata.md)
+- [Built-in integration](setup/index.md) for all major bundlers: [Vite](./setup/vite.md), [Next.js](./setup/next.md), [Webpack](./setup/webpack.md), `rollup`, `esbuild` and [another](./setup/other.md) with the power of [unplugin](https://github.com/unjs/unplugin)
+- Optional [grouping by folders](./group-and-hash.md)
+- Optimization with [svgo](./api/plugins/svgo.md)
+- [Automatically reset colors](./colors-reset.md)
+
+## Installation
+
+::: code-group
+
+```bash [npm]
+npm install -D @neodx/svg
+```
+
+```bash [yarn]
+yarn add -D @neodx/svg
+```
+
+```bash [pnpm]
+pnpm add -D @neodx/svg
+```
+
:::
-Powerful lightweight logger for new level of experience.
+## Getting started
+
+### 1. Setup your bundler
+
+First of all, you need to integrate one of our [plugins](./setup/) into your bundler and configure it:
+
+- [Vite](./setup/vite.md)
+- [Next.js](./setup/next.md)
+- [Webpack](./setup/webpack.md)
+- [Other](./setup/other.md)
+
+For example, `Vite` configuration will look like this:
+
+```typescript [vite.config.ts]
+import { defineConfig } from 'vite';
+import svg from '@neodx/svg/vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+ plugins: [
+ react(),
+ svg({
+ root: 'assets',
+ group: true,
+ output: 'public/sprites',
+ metadata: 'src/sprite.gen.ts'
+ })
+ ]
+});
+```
+
+Now, sprites will be built at the start of your `build`/`dev` command and any changes in the source folder(s) will initiate an incremental rebuild in `dev`/`watch` mode.
+
+For example, you will get the following structure:
+
+```diff
+/
+âââ assets
+â âââ common
+â â âââ left.svg
+â â âââ right.svg
+â âââ actions
+â âââ close.svg
+âââ public
++ âââ sprites
++ âââ common.svg
++ âââ actions.svg
+âââ src
++ âââ sprite.gen.ts
+```
+
+### 2. Create an Icon component
+
+Next, you need to create a single component that will be responsible for rendering icons, visit our ["Writing an Icon component"](./writing-icon-component.md) guide for more information.
+
+At the end, you can use your `Icon` component in any place of your application:
+
+```tsx [some-component.tsx]
+import { Icon } from './icon';
+
+export function SomeComponent() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Small description example
+
+
+
+ );
+}
+```
+
+In the result of this funny stuff, you will get something like this:
+
+![Example of using icons](/crazy-svg-mix.png)
-- TypeScript support out of box - generated types and information about your sprites
-- [Built-in integration](./frameworks-and-bundlers.md) for all major bundlers: `vite`, `webpack`, `rollup`, `esbuild` and another with the power of [unplugin](https://github.com/unjs/unplugin)
-- Optional grouping by folders
-- Optimization with svgo
-- Flexible colors reset
-- Powerful files selection
+Enjoy! ð
diff --git a/apps/docs/svg/metadata.md b/apps/docs/svg/metadata.md
new file mode 100644
index 0000000..0671dde
--- /dev/null
+++ b/apps/docs/svg/metadata.md
@@ -0,0 +1,142 @@
+# Generate metadata
+
+To build well-designed work with icons, we need to close next issues:
+
+- Type safety for icon names
+- [Grouping and hashing](./group-and-hash.md)
+ - Icons should be grouped in multiple sprites to prevent bloating of a single sprite
+ - Generated sprite file names should contain hash to prevent caching issues
+
+## Configuration
+
+To solve these problems, we're generating metadata for runtime usage what could be enabled by `metadata` option:
+
+::: code-group
+
+```typescript {13-19} [vite.config.ts]
+import svg from '@neodx/svg/vite';
+
+export default defineConfig({
+ plugins: [
+ svg({
+ root: 'assets',
+ output: 'public/sprites',
+ // group icons by sprite name
+ group: true,
+ // add hash to sprite file name
+ fileName: '{name}.{hash:8}.svg',
+ // generate metadata (width, height, viewBox, and sprite file path)
+ metadata: {
+ path: 'src/sprite.gen.ts',
+ runtime: {
+ size: true,
+ viewBox: true
+ }
+ }
+ })
+ ]
+});
+```
+
+:::
+
+In the result, we'll get `src/sprite.gen.ts` file with something like this:
+
+```typescript
+// Name could be changed by `metadata.types.name` option
+export interface SpritesMap {
+ 'sprite-name': 'left' | 'right' | 'close';
+}
+
+export const SPRITES_META = {
+ 'sprite-name': {
+ // `filePath` is a path to sprite file relative to `output` option
+ filePath: 'sprites.12345678.svg',
+ items: {
+ left: {
+ viewBox: '0 0 24 24',
+ width: 24,
+ height: 24
+ },
+ right: {
+ viewBox: '0 0 24 24',
+ width: 24,
+ height: 24
+ },
+ close: {
+ viewBox: '0 0 24 24',
+ width: 24,
+ height: 24
+ }
+ }
+ }
+};
+```
+
+## Support metadata in your code
+
+As you can see, we have `SpritesMap` with simple name mapping and `SPRITES_META` variable with runtime metadata.
+
+Let's write an example of how to handle this metadata in your code:
+
+::: code-group
+
+```tsx {1-2,6-7,11-12,16,21,26} [icon.tsx]
+import { getIconMeta } from './get-icon-meta';
+import type { SpritesMap } from './sprite.gen';
+import type { SVGProps } from 'react';
+
+export interface IconProps extends SVGProps {
+ sprite: T;
+ name: SpritesMap[T];
+}
+
+export function Icon({
+ sprite,
+ name,
+ className,
+ ...props
+}: IconProps) {
+ const { viewBox, filePath } = getIconMeta(sprite, name);
+
+ return (
+
+ );
+}
+```
+
+```typescript [get-icon-meta.ts]
+import { type SpritesMap, SPRITES_META } from './sprite.gen';
+
+export function getIconMeta(
+ sprite: T,
+ name: SpritesMap[T]
+): SpritesMap[T] {
+ const { filePath, items } = SPRITES_META[sprite];
+
+ return {
+ filePath,
+ ...items[name]
+ };
+}
+```
+
+:::
+
+However, you could see a huge problem here: now we should pass both `sprite` and `name` props for each icon! ðĪŊ
+
+Of course, it's a bad solution with terrible DX and various hacks in the future, let's fix it, check out the [Writing Icon Component](./writing-icon-component.md) guide to learn how to do it.
+
+## Related
+
+- ["Writing Icon Component" guide](./writing-icon-component.md)
+- ["Grouping and hashing" guide](./group-and-hash.md)
+- [`metadata` API Reference](./api/plugins/metadata.md)
diff --git a/apps/docs/svg/motivation.md b/apps/docs/svg/motivation.md
index 5b8b0c8..e602f6b 100644
--- a/apps/docs/svg/motivation.md
+++ b/apps/docs/svg/motivation.md
@@ -21,4 +21,5 @@ That's why we're here! ðĨģ
## Additional references
-- https://kurtextrem.de/posts/svg-in-js
+- https://kurtextrem.de/posts/svg-in-js: Great article about problems of SVG in JS
+- https://github.com/DavidWells/icon-pipeline: Simple solution for inlined sprites
diff --git a/apps/docs/svg/multicolored.md b/apps/docs/svg/multicolored.md
new file mode 100644
index 0000000..56f79fc
--- /dev/null
+++ b/apps/docs/svg/multicolored.md
@@ -0,0 +1,68 @@
+# Working with multiple colors
+
+Let's imagine that we have a really different icons with next requirements:
+
+- We have some known list of the accent colors, and we want to specify them in our CSS
+- All other colors should be inherited from the parent (for example, `currentColor`)
+
+## Configure `resetColors` option
+
+::: tip
+
+In [previous guide](./colors-reset.md) we've explained `resetColors` option in details.
+
+:::
+
+```typescript
+import svg from '@neodx/svg/vite';
+
+svg({
+ // ...
+ resetColors: {
+ // 1. Define known accent colors
+ replace: {
+ from: ['#6C707E', '#A8ADBD', '#818594'],
+ to: 'var(--icon-accent-color)'
+ },
+ // 2. Replace all other colors with `currentColor`
+ replaceUnknown: 'currentColor'
+ }
+});
+```
+
+## Add CSS variables
+
+```css
+/* shared/ui/index.css */
+
+@layer base {
+ :root {
+ /* make default accent color */
+ --icon-primary-color: #6c707e;
+ }
+}
+```
+
+## Usage
+
+Dirty but works ðŦĒ
+
+Probably, you can find a better solution ðŦ
+
+```tsx
+import { Icon } from '@/shared/ui';
+
+export function SomeFeature() {
+ return (
+
+ );
+}
+```
+
+## Related
+
+- ["Automatically reset colors" guide](./colors-reset.md)
+- ["Writing Icon Component" guide](./writing-icon-component.md)
diff --git a/apps/docs/svg/setup/index.md b/apps/docs/svg/setup/index.md
new file mode 100644
index 0000000..420f6e7
--- /dev/null
+++ b/apps/docs/svg/setup/index.md
@@ -0,0 +1,14 @@
+# Setup `@neodx/svg`
+
+## Documented integrations
+
+- [Vite with React](./vite.md)
+- [Next.js](./next.md)
+- [Webpack](./webpack.md)
+- [Other bundlers](./other.md)
+
+## Additional guides
+
+- [Grouping icons](../group-and-hash.md)
+- [Generating metadata](../metadata.md)
+- [Writing `Icon` component](../writing-icon-component)
diff --git a/apps/docs/svg/setup/next.md b/apps/docs/svg/setup/next.md
new file mode 100644
index 0000000..47efadf
--- /dev/null
+++ b/apps/docs/svg/setup/next.md
@@ -0,0 +1,74 @@
+# Setup `@neodx/svg` with [Next.js](https://nextjs.org/)
+
+::: tip Example repository
+You can visit ["examples/svg-next"](https://github.com/secundant/neodx/tree/main/examples/svg-next) project in our repository to see how it works.
+:::
+
+::: warning
+We don't provide specific adapter for Next.js, [`@neodx/svg/webpack` plugin](./webpack.md) will be used.
+:::
+
+## 1. Configure your assets
+
+Add `@neodx/svg/webpack` plugin to `next.config.js` and describe your svg assets location and output.
+
+::: code-group
+
+```javascript [next.config.js]
+const svg = require('@neodx/svg/webpack');
+
+module.exports = {
+ webpack: (config, { isServer }) => {
+ // Prevent doubling svg plugin, let's run it only for client build
+ if (!isServer) {
+ config.plugins.push(
+ svg({
+ root: 'assets',
+ output: 'public'
+ })
+ );
+ }
+ return config;
+ }
+};
+```
+
+:::
+
+## 2. Create your `Icon` component
+
+Visit our [Writing `Icon` component](../writing-icon-component) guide to see detailed instructions for creating `Icon` component.
+
+The simplest variant of `Icon` component will look like this:
+
+```tsx [icon.jsx]
+import clsx from 'clsx';
+
+export function Icon({ name, className, ...props }) {
+ return (
+
+ );
+}
+```
+
+## 3. Use your `Icon` component
+
+```tsx [some-component.tsx]
+import { Icon } from '@/shared/ui/icon';
+
+export function SomeComponent() {
+ return ;
+}
+```
+
+## Next steps
+
+- Read about [Grouping icons](../group-and-hash.md) and [Generating metadata](../metadata.md)
+- Learn about [Writing `Icon` component](../writing-icon-component) in detail
diff --git a/apps/docs/svg/setup/other.md b/apps/docs/svg/setup/other.md
new file mode 100644
index 0000000..d2f28d1
--- /dev/null
+++ b/apps/docs/svg/setup/other.md
@@ -0,0 +1,80 @@
+# Setup other
+
+We're using [unplugin](https://github.com/unjs/unplugin), so you can use any plugin that it supports.
+
+## Webpack
+
+::: code-group
+
+```typescript [webpack.config.js]
+const svg = require('@neodx/svg/webpack');
+
+modul.exports = {
+ plugins: [
+ svg({
+ root: 'assets',
+ output: 'public'
+ })
+ ]
+};
+```
+
+:::
+
+## Rollup
+
+::: code-group
+
+```typescript [rollup.config.mjs]
+import svg from '@neodx/svg/rollup';
+
+export default {
+ plugins: [
+ svg({
+ root: 'assets',
+ output: 'public'
+ })
+ ]
+};
+```
+
+:::
+
+## ESBuild
+
+::: code-group
+
+```typescript [esbuild.config.js]
+import { build } from 'esbuild';
+import svg from '@neodx/svg/esbuild';
+
+build({
+ plugins: [
+ svg({
+ root: 'assets',
+ output: 'public'
+ })
+ ]
+});
+```
+
+:::
+
+## RSPack
+
+::: code-group
+
+```typescript [rspack.config.js]
+const svg = require('@neodx/svg/rspack');
+
+modul.exports = {
+ plugins: [
+ svg({
+ root: 'assets',
+ output: 'public'
+ })
+ ]
+};
+```
+
+:::
diff --git a/apps/docs/svg/setup/vite.md b/apps/docs/svg/setup/vite.md
new file mode 100644
index 0000000..158b250
--- /dev/null
+++ b/apps/docs/svg/setup/vite.md
@@ -0,0 +1,68 @@
+# Setup `@neodx/svg` with [Vite](https://vitejs.dev/)
+
+::: tip Example repository
+You can visit ["examples/svg-vite"](https://github.com/secundant/neodx/tree/main/examples/svg-vite) project in our repository to see how it works.
+:::
+
+## 1. Configure your assets
+
+Add `@neodx/svg/vite` plugin to `vite.config.ts` and describe your svg assets location and output.
+
+::: code-group
+
+```typescript {3,8-11} [vite.config.ts]
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import svg from '@neodx/svg/vite';
+
+export default defineConfig({
+ plugins: [
+ react(),
+ svg({
+ // A "root" directory will be used to search for svg files
+ root: 'assets/icons',
+ output: 'public'
+ })
+ ]
+});
+```
+
+:::
+
+## 2. Create your `Icon` component
+
+Visit our [Writing `Icon` component](../writing-icon-component) guide to see detailed instructions for creating `Icon` component.
+
+The simplest variant of `Icon` component will look like this:
+
+```tsx [icon.jsx]
+import clsx from 'clsx';
+
+export function Icon({ name, className, ...props }) {
+ return (
+
+ );
+}
+```
+
+## 3. Use your `Icon` component
+
+```tsx [some-component.tsx]
+import { Icon } from '@/shared/ui/icon';
+
+export function SomeComponent() {
+ return ;
+}
+```
+
+## Next steps
+
+- Read about [Grouping icons](../group-and-hash.md) and [Generating metadata](../metadata.md)
+- Learn about [Writing `Icon` component](../writing-icon-component) in detail
diff --git a/apps/docs/svg/setup/webpack.md b/apps/docs/svg/setup/webpack.md
new file mode 100644
index 0000000..4e8a1b4
--- /dev/null
+++ b/apps/docs/svg/setup/webpack.md
@@ -0,0 +1,60 @@
+# Setup `@neodx/svg` with [Webpack](https://webpack.js.org/)
+
+## 1. Configure your assets
+
+Add `@neodx/svg/webpack` plugin to `webpack.config.js` and describe your svg assets location and output.
+
+::: code-group
+
+```javascript [webpack.config.js]
+const svg = require('@neodx/svg/webpack');
+
+module.exports = {
+ plugins: [
+ svg({
+ root: 'assets',
+ output: 'public'
+ })
+ ]
+};
+```
+
+:::
+
+## 2. Create your `Icon` component
+
+Visit our [Writing `Icon` component](../writing-icon-component) guide to see detailed instructions for creating `Icon` component.
+
+The simplest variant of `Icon` component will look like this:
+
+```tsx [icon.jsx]
+import clsx from 'clsx';
+
+export function Icon({ name, className, ...props }) {
+ return (
+
+ );
+}
+```
+
+## 3. Use your `Icon` component
+
+```tsx [some-component.tsx]
+import { Icon } from '@/shared/ui/icon';
+
+export function SomeComponent() {
+ return ;
+}
+```
+
+## Next steps
+
+- Read about [Grouping icons](../group-and-hash.md) and [Generating metadata](../metadata.md)
+- Learn about [Writing `Icon` component](../writing-icon-component) in detail
diff --git a/apps/docs/svg/writing-icon-component.md b/apps/docs/svg/writing-icon-component.md
new file mode 100644
index 0000000..71598b3
--- /dev/null
+++ b/apps/docs/svg/writing-icon-component.md
@@ -0,0 +1,347 @@
+---
+outline: [2, 3]
+---
+
+# Writing an `Icon` component
+
+We don't provide any pre-made, ready-to-use components.
+Such a solution would be too limited and opinionated for user-specific needs.
+
+Instead, we offer a detailed yet simple guide on how to create your own components.
+
+::: info
+
+In this guide, we will use React, TypeScript, and Tailwind CSS.
+
+:::
+
+## Result component
+
+From the start, I will show you the final result of this guide to give you a better understanding of what we are going to achieve.
+
+- Supports [grouped sprites with generated file names](./group-and-hash.md)
+- [Type-safe `IconName`](#make-name-prop-type-safe) (format: `spriteName/iconName`) for autocompletion and convenient usage
+- [Autoscaling](#detect-icon-major-axis-for-correct-scaling) based on the icon's aspect ratio
+- Open to any extension for your needs!
+
+::: details We will work with grouped hashed sprites with generated types and metadata
+
+```diff
+/
+âââ assets
+â âââ common
+â â âââ left.svg
+â â âââ right.svg
+â âââ actions
+â âââ close.svg
+âââ public
++ âââ sprites
++ âââ common.12ghS6Uj.svg
++ âââ actions.1A34ks78.svg
+âââ src
++ âââ sprite.gen.ts
+```
+
+:::
+
+::: code-group
+
+```tsx [icon.tsx]
+import clsx from 'clsx';
+import type { SVGProps } from 'react';
+import { SPRITES_META, type SpritesMap } from './sprite.gen';
+
+// Our icon will extend an SVG element and accept all its props
+export interface IconProps extends SVGProps {
+ name: AnyIconName;
+}
+// Merging all possible icon names as `sprite/icon` string
+export type AnyIconName = { [Key in keyof SpritesMap]: IconName }[keyof SpritesMap];
+// Icon name for a specific sprite, e.g. "common/left"
+export type IconName = `${Key}/${SpritesMap[Key]}`;
+
+export function Icon({ name, className, ...props }: IconProps) {
+ const { viewBox, filePath, iconName, axis } = getIconMeta(name);
+
+ return (
+
+ );
+}
+
+/**
+ * A function to get and process icon metadata.
+ * It was moved out of the Icon component to prevent type inference issues.
+ */
+const getIconMeta = (name: IconName) => {
+ const [spriteName, iconName] = name.split('/') as [Key, SpritesMap[Key]];
+ const {
+ filePath,
+ items: {
+ [iconName]: { viewBox, width, height }
+ }
+ } = SPRITES_META[spriteName];
+ const axis = width === height ? 'xy' : width > height ? 'x' : 'y';
+
+ return { filePath, iconName, viewBox, axis };
+};
+```
+
+```css [styles.css]
+@layer components {
+ /*
+ Our base class for icons inherits the current text color and applies common styles.
+ We're using a specific component class to prevent potential style conflicts and utilize the [data-axis] attribute.
+ */
+ .icon {
+ @apply select-none fill-current inline-block text-inherit box-content;
+ }
+
+ /* Set icon size to 1em based on its aspect ratio, so we can use `font-size` to scale it */
+ .icon[data-axis*='x'] {
+ /* scale horizontally */
+ @apply w-[1em];
+ }
+
+ .icon[data-axis*='y'] {
+ /* scale vertically */
+ @apply h-[1em];
+ }
+}
+```
+
+```typescript [vite.config.ts]
+import svg from '@neodx/svg/vite';
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ plugins: [
+ svg({
+ root: 'assets',
+ // group icons by sprite name
+ group: true,
+ output: 'public/sprites',
+ // add hash to sprite file name
+ fileName: '{name}.{hash:8}.svg',
+ metadata: {
+ path: 'src/sprite.gen.ts',
+ // generate metadata
+ runtime: {
+ size: true,
+ viewBox: true
+ }
+ }
+ })
+ ]
+});
+```
+
+:::
+
+## Step by step
+
+### Create a minimal working component
+
+In the minimal approach, our component will accept only the `name` (any string) prop and render the icon using the `