Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/runtime/.size-limit.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[
{
"name": "All publics",
Comment thread
dangreen marked this conversation as resolved.
"path": "dist/index.js",
"import": "*",
"limit": "1 kB"
}
]
63 changes: 63 additions & 0 deletions packages/runtime/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# @srcset/runtime

[![ESM-only package][package]][package-url]
[![NPM version][npm]][npm-url]
[![Node version][node]][node-url]
[![Dependencies status][deps]][deps-url]
[![Install size][size]][size-url]
[![Build status][build]][build-url]
[![Coverage status][coverage]][coverage-url]

[package]: https://img.shields.io/badge/package-ESM--only-ffe536.svg
[package-url]: https://nodejs.org/api/esm.html

[npm]: https://img.shields.io/npm/v/%40srcset%2Fruntime.svg
[npm-url]: https://npmjs.com/package/@srcset/runtime

[node]: https://img.shields.io/node/v/%40srcset%2Fruntime.svg
[node-url]: https://nodejs.org

[deps]: https://img.shields.io/librariesio/release/npm/%40srcset%2Fruntime
[deps-url]: https://libraries.io/npm/%40srcset%2Fruntime

[size]: https://deno.bundlejs.com/badge?q=%40srcset%2Fruntime
[size-url]: https://bundlejs.com/?q=%40srcset%2Fruntime

[build]: https://img.shields.io/github/actions/workflow/status/TrigenSoftware/srcset/tests.yml?branch=main
[build-url]: https://github.com/TrigenSoftware/srcset/actions

[coverage]: https://img.shields.io/codecov/c/github/TrigenSoftware/srcset.svg
[coverage-url]: https://app.codecov.io/gh/TrigenSoftware/srcset

Browser runtime helpers of [srcset](https://github.com/TrigenSoftware/srcset#readme): srcset strings and attributes of image variants.

- 🖼 Build `srcset` strings and `<img>`/`<source>` attributes from image variants
- 🌳 Zero dependencies, tree-shakable, less than 1 kB

## Install

```bash
# pnpm
pnpm add @srcset/runtime
# yarn
yarn add @srcset/runtime
# npm
npm i @srcset/runtime
```

## Usage

```ts
import url, { src, srcSet } from './photo.jpg'
import { getImageProps, getSourceProps } from '@srcset/runtime'

const { src: imgSrc, srcSet: imgSrcSet } = getImageProps(src, srcSet)
// <img src={imgSrc} srcSet={imgSrcSet}/>

const sources = getSourceProps(srcSet)
// sources of <picture>, grouped by mime type and ordered by format efficiency
```

## Documentation

For more details, guides and API references, check out the [documentation website](https://srcset.js.org/api/runtime/).
16 changes: 16 additions & 0 deletions packages/runtime/oxlint.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { defineConfig } from '@trigen/oxlint'
import testConfig from '@trigen/oxlint-config/test'
import tsTypeCheckedConfig from '@trigen/oxlint-config/typescript-type-checked'
import rootConfig from '../../oxlint.config.ts'

export default defineConfig({
extends: [
rootConfig,
tsTypeCheckedConfig,
testConfig
],
env: {
node: false,
browser: true
}
})
72 changes: 72 additions & 0 deletions packages/runtime/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"name": "@srcset/runtime",
"type": "module",
"version": "1.0.0",
"description": "Tiny runtime helpers for working with generated srcsets.",
"author": {
"name": "Dan Onoshko",
"email": "danon0404@gmail.com",
"url": "https://github.com/dangreen"
},
"license": "MIT",
"homepage": "https://srcset.js.org/api/runtime/",
"funding": "https://ko-fi.com/dangreen",
"repository": {
"type": "git",
"url": "https://github.com/TrigenSoftware/srcset.git",
"directory": "packages/runtime"
},
"bugs": {
"url": "https://github.com/TrigenSoftware/srcset/issues"
},
"keywords": [
"srcset",
"image",
"picture",
"responsive",
"runtime"
],
"engines": {
"node": ">=22"
},
"sideEffects": false,
"exports": {
"./package.json": "./package.json",
".": "./src/index.ts"
},
"publishConfig": {
"exports": {
"./package.json": "./package.json",
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"directory": "package",
"linkDirectory": false
},
"files": [
"dist"
],
"scripts": {
"clear:package": "del ./package",
"clear:dist": "del ./dist",
"clear": "del ./package ./dist ./coverage",
"prepublishOnly": "run build clear:package clean-publish",
"postpublish": "pnpm clear:package",
"emitDeclarations": "tsc -p ./tsconfig.build.json --emitDeclarationOnly",
"build:dist": "run -p emitDeclarations [ vite build ]",
"build": "run clear:dist build:dist",
"lint": "oxlint",
"format": "oxlint --fix",
"test:unit": "vitest run --coverage",
"test:unit:watch": "vitest watch",
"test:size": "size-limit",
"test:types": "tsc --noEmit",
"test": "run -p lint test:unit test:types"
},
"devDependencies": {
"@size-limit/preset-small-lib": "^12.0.0",
"size-limit": "^12.0.0"
}
}
96 changes: 96 additions & 0 deletions packages/runtime/src/attributes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {
describe,
it,
expect
} from 'vitest'
import {
getSourceProps,
getImageProps
} from './attributes.ts'
import {
createSrc,
createSrcSet
} from '../test/src.mock.ts'

describe('runtime', () => {
describe('attributes', () => {
describe('getSourceProps', () => {
it('should group variants into sources ordered by format efficiency', () => {
const srcSet = [
...createSrcSet('jpg', [320, 640]),
...createSrcSet('webp', [320, 640]),
...createSrcSet('avif', [320, 640])
]

expect(getSourceProps(srcSet)).toEqual([
{
type: 'image/avif',
srcSet: '/images/image@320w.avif 320w, /images/image@640w.avif 640w'
},
{
type: 'image/webp',
srcSet: '/images/image@320w.webp 320w, /images/image@640w.webp 640w'
},
{
type: 'image/jpeg',
srcSet: '/images/image@320w.jpg 320w, /images/image@640w.jpg 640w'
}
])
})

it('should return empty array for empty set', () => {
expect(getSourceProps([])).toEqual([])
})
})

describe('getImageProps', () => {
it('should make src with srcset of the same format', () => {
const src = createSrc('jpg', 640)
const srcSet = [
...createSrcSet('jpg', [320, 640]),
...createSrcSet('webp', [320, 640])
]

expect(getImageProps(src, srcSet)).toEqual({
src: '/images/image@640w.jpg',
srcSet: '/images/image@320w.jpg 320w, /images/image@640w.jpg 640w'
})
})

it('should omit srcset when it duplicates src url', () => {
const src = createSrc('jpg', 640)

expect(getImageProps(src, [src])).toEqual({
src: '/images/image@640w.jpg'
})
})

it('should omit srcset without variants of the same format', () => {
const src = createSrc('jpg', 640)

expect(getImageProps(src, createSrcSet('webp', [320]))).toEqual({
src: '/images/image@640w.jpg'
})
})

it('should pass srcset string through', () => {
const src = createSrc('jpg', 640)

expect(getImageProps(src, 'custom 1x')).toEqual({
src: '/images/image@640w.jpg',
srcSet: 'custom 1x'
})
})

it('should make srcset without src', () => {
expect(getImageProps(undefined, createSrcSet('jpg', [320, 640]))).toEqual({
srcSet: '/images/image@320w.jpg 320w, /images/image@640w.jpg 640w'
})
})

it('should return empty props without src and srcset', () => {
expect(getImageProps(undefined, undefined)).toEqual({})
})
})
})
})
92 changes: 92 additions & 0 deletions packages/runtime/src/attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import type { SrcSetEntry } from './types.ts'
import {
filterBy,
groupBy,
toSrcSetString
} from './utils.ts'

function getTypePriority(type: string) {
if (type === 'image/avif') {
return 0
}

if (type === 'image/webp') {
return 1
}

return 2
}

function compareSourceGroups(
[typeA]: [string, SrcSetEntry[]],
[typeB]: [string, SrcSetEntry[]]
) {
return getTypePriority(typeA) - getTypePriority(typeB)
}
Comment thread
dangreen marked this conversation as resolved.

/**
* Make props for `<source>` elements of a `<picture>`.
* Groups are ordered by format efficiency (avif, webp, then the rest),
* so a browser picks the best supported format first.
* @param srcSet - Set of image variants.
* @returns Props for `<source>` elements.
*/
export function getSourceProps(srcSet: SrcSetEntry[]) {
return groupBy(srcSet, 'type')
.sort(compareSourceGroups)
.map(([type, group]) => ({
type,
srcSet: toSrcSetString(group)
}))
}

export interface ImageSrcProps {
src?: string
srcSet?: string
}

/**
* Make `src` and `srcSet` attribute values for the `<img>` element.
* The `srcSet` string is built from the variants of the `src` format
* and omitted when it duplicates the `src` url.
* @param src - Image variant for the `src` attribute.
* @param srcSet - Set of image variants or a ready `srcset` string.
* @returns `src` and `srcSet` attribute values.
*/
export function getImageProps(src: SrcSetEntry | undefined, srcSet: SrcSetEntry[] | string | undefined): ImageSrcProps {
if (typeof srcSet === 'string') {
return src
? {
src: src.url,
srcSet
}
: {
srcSet
}
}
Comment thread
dangreen marked this conversation as resolved.
Comment thread
dangreen marked this conversation as resolved.

if (!src) {
if (srcSet) {
return {
srcSet: toSrcSetString(srcSet)
}
}

return {}
}
Comment thread
dangreen marked this conversation as resolved.

if (srcSet) {
const srcSetString = toSrcSetString(filterBy(srcSet, 'format', src.format))

if (srcSetString && srcSetString !== src.url) {
return {
src: src.url,
srcSet: srcSetString
}
}
}

return {
src: src.url
}
}
23 changes: 23 additions & 0 deletions packages/runtime/src/formats.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {
describe,
it,
expect
} from 'vitest'
import { mimeTypes } from './formats.ts'

describe('runtime', () => {
describe('formats', () => {
describe('mimeTypes', () => {
it('should map formats to mime types', () => {
expect(mimeTypes).toEqual({
avif: 'image/avif',
webp: 'image/webp',
jpg: 'image/jpeg',
png: 'image/png',
gif: 'image/gif',
svg: 'image/svg+xml'
})
})
})
})
})
13 changes: 13 additions & 0 deletions packages/runtime/src/formats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { ImageFormat } from './types.ts'

/**
* Mime types of supported image formats.
*/
export const mimeTypes: Record<ImageFormat, string> = {
avif: 'image/avif',
webp: 'image/webp',
jpg: 'image/jpeg',
png: 'image/png',
gif: 'image/gif',
svg: 'image/svg+xml'
}
Comment thread
dangreen marked this conversation as resolved.
4 changes: 4 additions & 0 deletions packages/runtime/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type * from './types.ts'
export * from './formats.ts'
export * from './utils.ts'
export * from './attributes.ts'
Loading