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 + +preview + +::: 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) + +preview + +`@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 + +preview + +## 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) +Log preview -- **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 + +errors preview + +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! 🎉 + +preview + +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 + +pretty errors output example + +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 + +pretty errors with .cause output example + +### 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 😁 + +pretty errors disabled output example + +## 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 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + + 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 ( + + {/* For example, "/sprites/common.svg#favourite". Change a base path if you don't store sprites under the "/sprites". */} + + + ); +} + +/** + * 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 `` element. + +::: code-group + +```tsx [icon.tsx] +import clsx from 'clsx'; +import type { SVGProps } from 'react'; + +export interface IconProps extends SVGProps { + name: string; +} + +export function Icon({ name, className, viewBox, ...props }: IconProps) { + return ( + + + + ); +} +``` + +```tsx [some-component.tsx] +import { Icon } from './icon'; + +export function SomeComponent() { + return ( +
+ +
+ ); +} +``` + +::: + +It works, but we could see some issues: + +- `name` prop is not type-safe, we can pass any string +- `viewBox` is missing, some icons may be rendered incorrectly +- `/sprite.svg` is hardcoded, we can't use hashed file names or grouped sprites +- `classNames` contain `w-[1em] h-[1em]` styles, we can't use non-square icons (for example, logos) + +Let's fix them! + +### Make `name` prop type-safe + +::: warning +In this guide I'll use `spriteName/iconName` format to name icons, but it will be broken if you use `/` in your icon or sprite name (for example, nested names), be careful. +::: + +As we faced in the [metadata generation guide](./metadata.md), we can use the generated `SpritesMap` type, but, we should keep a single `name` property for the DX and simplicity reasons. + +Let's start implementing it: + +1. Declare `IconName` type + + ```ts + import { type SpritesMap } from './sprite.gen'; + + // Icon name for a specific sprite + export type IconName = `${Key}/${SpritesMap[Key]}`; + ``` + +2. Write a function to get and process icon metadata + + ```ts + import { type SpritesMap, SPRITES_META } from './sprite.gen'; + + const getIconMeta = (name: IconName) => { + const [spriteName, iconName] = name.split('/') as [Key, SpritesMap[Key]]; + const { + filePath, + items: { + [iconName]: { viewBox } + } + } = SPRITES_META[spriteName]; + + return { filePath, iconName, viewBox }; + }; + ``` + +3. Update `Icon` component + + ```tsx {3,5-7,9,14,18,21} + import clsx from 'clsx'; + import type { SVGProps } from 'react'; + import { SPRITES_META, type SpritesMap } from './sprite.gen'; + + export interface IconProps extends SVGProps { + name: AnyIconName; + } + // All possible icon names + export type AnyIconName = { [Key in keyof SpritesMap]: IconName }[keyof SpritesMap]; + // Icon name for a specific sprite + export type IconName = `${Key}/${SpritesMap[Key]}`; + + export function Icon({ name /* ... */ }: IconProps) { + const { viewBox, filePath, iconName, axis } = getIconMeta(name); + + return ( + + + + ); + } + ``` + +### Detect icon major axis for correct scaling + +How will SVG be scaled if source asset size is non-square? It will be forced to be square! + +![wrong size](/wrong-svg-size.png) + +In the screenshot above, we can see that the right icon container is filled as a square, but the icon itself is not. + +We're expecting the left icon behavior, let's fix it! + +We already know `width` and `height` of the icon, so we can compare them and detect the major axis (or scale both axes if they are equal). +I'll extract icon styles to the global `@layer components` to use `[data-axis*=]` selector and make `Icon` component open for extension. + +::: code-group + +```css [styles.css] +@layer components { + .icon { + /* reset styles and prevent icon from being selected */ + @apply select-none fill-current inline-block text-inherit box-content; + } + + .icon[data-axis*='x'] { + /* scale horizontally */ + @apply w-[1em]; + } + + .icon[data-axis*='y'] { + /* scale vertically */ + @apply h-[1em]; + } +} +``` + +```tsx {2,6,11,22,25,27} [icon.tsx] +export function Icon({ name /* ... */ }: IconProps) { + const { viewBox, filePath, iconName, axis } = getIconMeta(name); + + return ( + + ); +} + +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 }; +}; +``` + +::: diff --git a/apps/docs/vfs/index.md b/apps/docs/vfs/index.md new file mode 100644 index 0000000..848e630 --- /dev/null +++ b/apps/docs/vfs/index.md @@ -0,0 +1,5 @@ +# @neodx/vfs + +::: danger WIP +`@neodx/vfs` is under redesign, please, check [npm package](https://www.npmjs.com/package/@neodx/vfs) for information about current API. +::: diff --git a/examples/log-frameworks-showcase/package.json b/examples/log-frameworks-showcase/package.json index 569a291..5354c75 100644 --- a/examples/log-frameworks-showcase/package.json +++ b/examples/log-frameworks-showcase/package.json @@ -17,15 +17,15 @@ }, "dependencies": { "@neodx/log": "workspace:*", - "express": "^4.18.2", - "koa": "^2.14.2", - "koa-router": "^12.0.0" + "express": "4.18.2", + "koa": "2.14.2", + "koa-router": "12.0.0" }, "devDependencies": { - "@types/express": "^4.17.17", - "@types/koa": "^2.13.6", - "@types/koa-router": "^7.4.4", - "tsx": "^3.12.7", - "typescript": "^5.1.6" + "@types/express": "4.17.18", + "@types/koa": "2.13.9", + "@types/koa-router": "7.4.5", + "tsx": "3.13.0", + "typescript": "5.2.2" } } diff --git a/examples/svg-magic-with-figma-export/package.json b/examples/svg-magic-with-figma-export/package.json index f49f35d..1667530 100644 --- a/examples/svg-magic-with-figma-export/package.json +++ b/examples/svg-magic-with-figma-export/package.json @@ -16,16 +16,16 @@ "react-dom": "18.2.0" }, "devDependencies": { - "@types/react": "^18.2.15", - "@types/react-dom": "^18.2.7", - "@vitejs/plugin-react": "^4.0.3", - "autoprefixer": "10.4.14", - "postcss": "8.4.26", + "@types/react": "18.2.25", + "@types/react-dom": "18.2.11", + "@vitejs/plugin-react": "4.1.0", + "autoprefixer": "10.4.16", + "postcss": "8.4.31", "tailwindcss": "3.3.3", - "typescript": "^5.1.6", - "vite": "^4.4.5", - "vite-tsconfig-paths": "^4.2.0", - "vitest": "^0.33.0" + "typescript": "5.2.2", + "vite": "4.4.11", + "vite-tsconfig-paths": "4.2.1", + "vitest": "0.34.6" }, "scripts": { "export-icons": "figma export --verbose", diff --git a/examples/svg-magic-with-figma-export/src/shared/ui/icon/sprite.gen.ts b/examples/svg-magic-with-figma-export/src/shared/ui/icon/sprite.gen.ts index d759da2..60f8a50 100644 --- a/examples/svg-magic-with-figma-export/src/shared/ui/icon/sprite.gen.ts +++ b/examples/svg-magic-with-figma-export/src/shared/ui/icon/sprite.gen.ts @@ -145,155 +145,7 @@ export interface SpritesMap { | 'writerside-preview' | 'writerside'; } -export const SPRITES_META = { - general: [ - 'add', - 'autoscroll-from-source', - 'autoscroll-to-source', - 'checkmark', - 'chevron-down-large', - 'chevron-down', - 'chevron-left', - 'chevron-right', - 'chevron-up-large', - 'chevron-up', - 'close-small-hovered', - 'close-small', - 'close', - 'collapse-all', - 'copy', - 'cut', - 'delete', - 'down', - 'download', - 'edit', - 'exit', - 'expand-all', - 'export', - 'external-link', - 'filter', - 'groups', - 'help', - 'hide', - 'history', - 'ide-update', - 'import', - 'layout', - 'left', - 'list-files', - 'locate', - 'locked', - 'more-horizontal', - 'more-vertical', - 'move-down', - 'move-up', - 'open-in-tool-window', - 'open-new-tab', - 'open', - 'pagination', - 'paste', - 'pin', - 'plugin-update', - 'preview-horizontally', - 'preview-vertically', - 'print', - 'project-structure', - 'project-wide-analysis-off', - 'project-wide-analysis-on', - 'redo', - 'refresh', - 'remove', - 'right', - 'run-anything', - 'save', - 'scroll-down', - 'search', - 'settings', - 'show-as-tree', - 'show', - 'soft-wrap', - 'sort-alphabetically', - 'sort-by-duration', - 'sort-by-type', - 'sort-by-usage', - 'sort-by-visibility', - 'sort-by', - 'split-horizontally', - 'split-vertically', - 'undo', - 'unlocked', - 'up', - 'upload', - 'vcs' - ], - 'tool-windows': [ - 'ant', - 'aws-glue', - 'bookmarks', - 'build-server-protocol', - 'build', - 'c-make-tool-window', - 'changes', - 'commit', - 'concurrency-diagram-toolwindow', - 'coverage', - 'cwm-access', - 'cwm-users', - 'database-changes', - 'dataproc-tool-window', - 'dbms', - 'debug', - 'dependencies', - 'documentation', - 'donate', - 'endpoints', - 'exception-analyzer', - 'find-external-usages', - 'find', - 'gitlab', - 'gradle', - 'hierarchy', - 'hive', - 'jupyter-tool-window', - 'kafka', - 'kotlin-tool-window', - 'learn', - 'makefile-tool-window', - 'maven', - 'messages', - 'new-u-i', - 'notifications', - 'npm', - 'package-manager', - 'problems', - 'profiler', - 'project', - 'pull-requests', - 'python-console-tool-window', - 'repositories', - 'run', - 'rust', - 'sbt-icon', - 'sbt-shell', - 'sci-view', - 'services', - 'setting-sync', - 'space-tool-window', - 'spring', - 'structure', - 'task', - 'terminal', - 'todo', - 'transfer', - 'unknown', - 'vcs', - 'web-locator', - 'web-server', - 'web', - 'writerside-preview', - 'writerside' - ] -} satisfies { +export const SPRITES_META: { general: Array< | 'add' | 'autoscroll-from-source' @@ -441,4 +293,152 @@ export const SPRITES_META = { | 'writerside-preview' | 'writerside' >; +} = { + general: [ + 'add', + 'autoscroll-from-source', + 'autoscroll-to-source', + 'checkmark', + 'chevron-down-large', + 'chevron-down', + 'chevron-left', + 'chevron-right', + 'chevron-up-large', + 'chevron-up', + 'close-small-hovered', + 'close-small', + 'close', + 'collapse-all', + 'copy', + 'cut', + 'delete', + 'down', + 'download', + 'edit', + 'exit', + 'expand-all', + 'export', + 'external-link', + 'filter', + 'groups', + 'help', + 'hide', + 'history', + 'ide-update', + 'import', + 'layout', + 'left', + 'list-files', + 'locate', + 'locked', + 'more-horizontal', + 'more-vertical', + 'move-down', + 'move-up', + 'open-in-tool-window', + 'open-new-tab', + 'open', + 'pagination', + 'paste', + 'pin', + 'plugin-update', + 'preview-horizontally', + 'preview-vertically', + 'print', + 'project-structure', + 'project-wide-analysis-off', + 'project-wide-analysis-on', + 'redo', + 'refresh', + 'remove', + 'right', + 'run-anything', + 'save', + 'scroll-down', + 'search', + 'settings', + 'show-as-tree', + 'show', + 'soft-wrap', + 'sort-alphabetically', + 'sort-by-duration', + 'sort-by-type', + 'sort-by-usage', + 'sort-by-visibility', + 'sort-by', + 'split-horizontally', + 'split-vertically', + 'undo', + 'unlocked', + 'up', + 'upload', + 'vcs' + ], + 'tool-windows': [ + 'ant', + 'aws-glue', + 'bookmarks', + 'build-server-protocol', + 'build', + 'c-make-tool-window', + 'changes', + 'commit', + 'concurrency-diagram-toolwindow', + 'coverage', + 'cwm-access', + 'cwm-users', + 'database-changes', + 'dataproc-tool-window', + 'dbms', + 'debug', + 'dependencies', + 'documentation', + 'donate', + 'endpoints', + 'exception-analyzer', + 'find-external-usages', + 'find', + 'gitlab', + 'gradle', + 'hierarchy', + 'hive', + 'jupyter-tool-window', + 'kafka', + 'kotlin-tool-window', + 'learn', + 'makefile-tool-window', + 'maven', + 'messages', + 'new-u-i', + 'notifications', + 'npm', + 'package-manager', + 'problems', + 'profiler', + 'project', + 'pull-requests', + 'python-console-tool-window', + 'repositories', + 'run', + 'rust', + 'sbt-icon', + 'sbt-shell', + 'sci-view', + 'services', + 'setting-sync', + 'space-tool-window', + 'spring', + 'structure', + 'task', + 'terminal', + 'todo', + 'transfer', + 'unknown', + 'vcs', + 'web-locator', + 'web-server', + 'web', + 'writerside-preview', + 'writerside' + ] }; diff --git a/examples/svg-next/README.md b/examples/svg-next/README.md index a45dade..f2e6f1f 100644 --- a/examples/svg-next/README.md +++ b/examples/svg-next/README.md @@ -33,7 +33,7 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the ## Configuration ```javascript -// next.config.mjs +// next.config.js import svg from '@neodx/svg/webpack'; /** @type {import('next').NextConfig} */ diff --git a/examples/svg-next/next.config.mjs b/examples/svg-next/next.config.js similarity index 70% rename from examples/svg-next/next.config.mjs rename to examples/svg-next/next.config.js index f27a263..543cb93 100644 --- a/examples/svg-next/next.config.mjs +++ b/examples/svg-next/next.config.js @@ -1,9 +1,7 @@ -// eslint-disable-next-line import/no-unresolved -import svg from '@neodx/svg/webpack'; +const svg = require('@neodx/svg/webpack'); /** @type {import('next').NextConfig} */ -const nextConfig = { - reactStrictMode: true, +module.exports = { webpack: (config, { isServer }) => { if (!isServer) { config.plugins.push( @@ -20,5 +18,3 @@ const nextConfig = { return config; } }; - -export default nextConfig; diff --git a/examples/svg-next/package.json b/examples/svg-next/package.json index 892754c..c55f86e 100644 --- a/examples/svg-next/package.json +++ b/examples/svg-next/package.json @@ -1,7 +1,6 @@ { "name": "example-svg-next", "version": "0.1.0", - "type": "module", "private": true, "scripts": { "dev": "next dev", @@ -10,23 +9,23 @@ "lint": "next lint" }, "dependencies": { - "@types/node": "20.4.2", - "@types/react": "18.2.15", - "@types/react-dom": "18.2.7", - "autoprefixer": "10.4.14", - "eslint": "8.45.0", - "eslint-config-next": "13.4.10", - "next": "13.4.10", - "postcss": "8.4.26", + "@types/node": "20.8.3", + "@types/react": "18.2.25", + "@types/react-dom": "18.2.11", + "autoprefixer": "10.4.16", + "eslint": "8.51.0", + "eslint-config-next": "13.5.4", + "next": "13.5.4", + "postcss": "8.4.31", "react": "18.2.0", "react-dom": "18.2.0", "tailwindcss": "3.3.3", - "typescript": "5.1.6" + "typescript": "5.2.2" }, "devDependencies": { "@neodx/svg": "workspace:*", - "@types/react": "^18.2.15", - "@types/react-dom": "^18.2.7" + "@types/react": "18.2.25", + "@types/react-dom": "18.2.11" }, "nx": { "targets": { diff --git a/examples/svg-next/public/sprite.svg b/examples/svg-next/public/sprite.svg index f1106a3..7ec6524 100644 --- a/examples/svg-next/public/sprite.svg +++ b/examples/svg-next/public/sprite.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/examples/svg-next/src/shared/ui/icon/sprite.gen.ts b/examples/svg-next/src/shared/ui/icon/sprite.gen.ts index d7542db..dae59a0 100644 --- a/examples/svg-next/src/shared/ui/icon/sprite.gen.ts +++ b/examples/svg-next/src/shared/ui/icon/sprite.gen.ts @@ -10,19 +10,7 @@ export interface SpritesMap { | 'checkmark' | 'commit'; } -export const SPRITES_META = { - sprite: [ - 'add', - 'autoscroll-from-source', - 'autoscroll-to-source', - 'build-server-protocol', - 'build', - 'c-make-tool-window', - 'changes', - 'checkmark', - 'commit' - ] -} satisfies { +export const SPRITES_META: { sprite: Array< | 'add' | 'autoscroll-from-source' @@ -34,4 +22,16 @@ export const SPRITES_META = { | 'checkmark' | 'commit' >; +} = { + sprite: [ + 'add', + 'autoscroll-from-source', + 'autoscroll-to-source', + 'build-server-protocol', + 'build', + 'c-make-tool-window', + 'changes', + 'checkmark', + 'commit' + ] }; diff --git a/examples/svg-vite/README.md b/examples/svg-vite/README.md index ea66613..ee9a59a 100644 --- a/examples/svg-vite/README.md +++ b/examples/svg-vite/README.md @@ -2,6 +2,12 @@ This example shows how to use `@neodx/svg` as Vite plugin and simple step-by-step setup for React. +Based on next guides which you can find in our [documentation](https://neodx.github.io/svg): + +- [Writing an `Icon` component](https://neodx.github.io/svg/writing-icon-component) +- [Setup Vite](https://neodx.github.io/svg/setup/vite) +- [Working with multicolored icons](https://neodx.github.io/svg/multicolored) + In addition, you can see how to use multicolored icons with TailwindCSS and CSS variable (it's not very pleasant, but it works 🌝). @@ -35,7 +41,8 @@ export default defineConfig(({ command }) => ({ svg({ root: 'assets', // Root folder for SVG files, all source paths will be relative to this folder group: true, // Group SVG files by folder - output: 'public', // Output folder for generated files + output: 'public/sprites', // Output folder for generated files + fileName: '{name}.{hash:8}.svg', // Add hash to file name metadata: { path: 'src/shared/ui/icon/sprite.gen.ts', // Output file for generated TypeScript definitions runtime: { @@ -45,6 +52,7 @@ export default defineConfig(({ command }) => ({ } }, resetColors: { + exclude: [/^flags/, /^logos/], // Exclude some icons from color reset replace: ['#000', '#eee', '#6C707E'], // Resets all known colors to `currentColor` replaceUnknown: 'var(--icon-color)' // Replaces unknown colors with custom CSS variable } @@ -62,43 +70,56 @@ import clsx from 'clsx'; import type { SVGProps } from 'react'; import { SPRITES_META, type SpritesMap } from './sprite.gen'; -// Merging all icons as `SPRITE_NAME/ICON_NAME` -export type SpriteKey = { - [Key in keyof SpritesMap]: `${Key}/${SpritesMap[Key]}`; -}[keyof SpritesMap]; - -export interface IconProps extends Omit, 'name' | 'type'> { - name: SpriteKey; +// 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, viewBox, ...props }: IconProps) { - const [spriteName, iconName] = name.split('/') as [ - keyof SpritesMap, - SpritesMap[keyof SpritesMap] - ]; - const { filePath, items } = SPRITES_META[spriteName]; - // @ts-expect-error mixed structures are confusing TS - const { viewBox, width, height } = items[iconName]; - const rect = width === height ? 'xy' : width > height ? 'x' : 'y'; +export function Icon({ name, className, ...props }: IconProps) { + const { viewBox, filePath, iconName, axis } = getIconMeta(name); return ( - + {/* For example, "/sprites/common.svg#favourite". Change a base path if you don't store sprites under the "/sprites". */} + ); } + +/** + * 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 }; +}; ``` [shared/ui/index.css](./src/shared/ui/index.css): @@ -110,26 +131,27 @@ export function Icon({ name, className, viewBox, ...props }: IconProps) { @layer base { :root { - /* By default, all icons will inherit color from parent, but we can override it */ --icon-color: currentColor; } } @layer components { - /* Our base class for all icons */ + /* + 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; } - .icon[data-icon-aspect-ratio='xy'] { - @apply w-[1em] h-[1em]; - } - - .icon[data-icon-aspect-ratio='x'] { + /* 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-icon-aspect-ratio='y'] { + .icon[data-axis*='y'] { + /* scale vertically */ @apply h-[1em]; } } diff --git a/examples/svg-vite/assets/common/download.svg b/examples/svg-vite/assets/tool/download.svg similarity index 100% rename from examples/svg-vite/assets/common/download.svg rename to examples/svg-vite/assets/tool/download.svg diff --git a/examples/svg-vite/assets/common/history.svg b/examples/svg-vite/assets/tool/history.svg similarity index 100% rename from examples/svg-vite/assets/common/history.svg rename to examples/svg-vite/assets/tool/history.svg diff --git a/examples/svg-vite/assets/common/import.svg b/examples/svg-vite/assets/tool/import.svg similarity index 100% rename from examples/svg-vite/assets/common/import.svg rename to examples/svg-vite/assets/tool/import.svg diff --git a/examples/svg-vite/package.json b/examples/svg-vite/package.json index f9fba90..bbbed22 100644 --- a/examples/svg-vite/package.json +++ b/examples/svg-vite/package.json @@ -8,21 +8,21 @@ "access": "restricted" }, "dependencies": { - "@fontsource-variable/inter": "^5.0.5", + "@fontsource-variable/inter": "5.0.13", "react": "18.2.0", "react-dom": "18.2.0" }, "devDependencies": { "@neodx/svg": "workspace:*", - "@types/react": "^18.2.15", - "@types/react-dom": "^18.2.7", - "@vitejs/plugin-react": "^4.0.3", - "autoprefixer": "10.4.14", - "postcss": "8.4.26", + "@types/react": "18.2.25", + "@types/react-dom": "18.2.11", + "@vitejs/plugin-react": "4.1.0", + "autoprefixer": "10.4.16", + "postcss": "8.4.31", "tailwindcss": "3.3.3", - "typescript": "^5.1.6", - "vite": "^4.4.5", - "vite-tsconfig-paths": "^4.2.0" + "typescript": "5.2.2", + "vite": "4.4.11", + "vite-tsconfig-paths": "4.2.1" }, "scripts": { "dev": "vite", diff --git a/examples/svg-vite/public/common.7854f9b0.svg b/examples/svg-vite/public/common.7854f9b0.svg deleted file mode 100644 index a5b35d9..0000000 --- a/examples/svg-vite/public/common.7854f9b0.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/svg-vite/public/sprites/common.4b814dcf.svg b/examples/svg-vite/public/sprites/common.4b814dcf.svg new file mode 100644 index 0000000..eadfaea --- /dev/null +++ b/examples/svg-vite/public/sprites/common.4b814dcf.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/svg-vite/public/flags.def2b6af.svg b/examples/svg-vite/public/sprites/flags.def2b6af.svg similarity index 100% rename from examples/svg-vite/public/flags.def2b6af.svg rename to examples/svg-vite/public/sprites/flags.def2b6af.svg diff --git a/examples/svg-vite/public/logos.6c76e8cd.svg b/examples/svg-vite/public/sprites/logos.6c76e8cd.svg similarity index 100% rename from examples/svg-vite/public/logos.6c76e8cd.svg rename to examples/svg-vite/public/sprites/logos.6c76e8cd.svg diff --git a/examples/svg-vite/public/sprites/tool.1f541a98.svg b/examples/svg-vite/public/sprites/tool.1f541a98.svg new file mode 100644 index 0000000..5a27489 --- /dev/null +++ b/examples/svg-vite/public/sprites/tool.1f541a98.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/svg-vite/src/app/app.tsx b/examples/svg-vite/src/app/app.tsx index 1fd625d..1986d2a 100644 --- a/examples/svg-vite/src/app/app.tsx +++ b/examples/svg-vite/src/app/app.tsx @@ -1,17 +1,40 @@ -import { keys } from '@neodx/std'; +import { entries, keys } from '@neodx/std'; import clsx from 'clsx'; import { useState } from 'react'; -import type { IconName } from '../shared/ui/icon'; -import { Icon, SPRITES_META } from '../shared/ui/icon'; +import { type AnyIconName, Icon, SPRITES_META } from '../shared/ui/icon'; export function App() { - const [selected, setSelected] = useState('common/favourite'); + const [selected, setSelected] = useState('logos/twitter'); return (
+
+
+ + + + + +
+
+ + + + +
+ + + Small description example + + +
+

- Playground + Playground

Multicolor icon

@@ -24,19 +47,31 @@ export function App() {
- +
+
+

OK

+ +
+
+

Bad

+ +
+