Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,4 @@ jobs:
node-version: 22
cache: 'pnpm'
- name: Check package.json files
run: pnpm --package=@trigen/lint-package-json dlx lint-package-json --monorepo
run: pnpm --package=@trigen/lint-package-json dlx lint-package-json --monorepo --ignore examples
101 changes: 101 additions & 0 deletions examples/rspack/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>srcset + Rspack</title>
<style>
body {
margin: 0 auto;
padding: 2rem 1rem 4rem;
max-width: 48rem;
font-family: system-ui, sans-serif;
line-height: 1.5;
color: #1c1c1e;
background: #fdfdfd;
}

h1 {
margin-bottom: .25rem;
}

.intro {
margin-top: 0;
color: #55555c;
}

section {
margin: 2.5rem 0;
padding-top: 1.5rem;
border-top: 1px solid #e3e3e6;
}

h2 {
font-size: 1.1rem;
margin: 0 0 .75rem;
}

picture {
display: block;
}

img {
max-width: 100%;
height: auto;
display: block;
margin-inline: auto;
border-radius: 6px;
background-color: #ececef;
}

.caption {
margin: .5rem 0 0;
font-size: .9rem;
color: #55555c;
}

.variants {
margin: .75rem 0 0;
padding: .5rem .75rem;
list-style: none;
font-size: .75rem;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
color: #3c3c43;
background: #f2f2f5;
border-radius: 6px;
overflow-x: auto;
}

.variants li {
white-space: nowrap;
}

/* Makes the transparency of the sticker visible. */
picture.checkerboard {
display: flex;
justify-content: center;
overflow: hidden;
border-radius: 6px;
background-color: #fff;
background-image:
linear-gradient(45deg, #dedee4 25%, transparent 25%, transparent 75%, #dedee4 75%),
linear-gradient(45deg, #dedee4 25%, transparent 25%, transparent 75%, #dedee4 75%);
background-size: 24px 24px;
background-position: 0 0, 12px 12px;
}

picture.checkerboard img {
background-color: transparent;
}
</style>
</head>
<body>
<h1>srcset + Rspack</h1>
<p class="intro">
Four image imports processed by <code>@srcset/loader</code> at build time, wired through
<code>module.rules</code> of Rspack and rendered with the <code>Picture</code> and
<code>Image</code> components of <code>@srcset/react</code>.
</p>
<main id="app"></main>
</body>
</html>
10 changes: 10 additions & 0 deletions examples/rspack/oxlint.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from '@trigen/oxlint'
import rootConfig from '../../oxlint.config.ts'

export default defineConfig({
extends: [rootConfig],
// The bundler config runs in node, the entry code runs in a browser.
env: {
browser: true
}
})
21 changes: 21 additions & 0 deletions examples/rspack/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@srcset/rspack-example",
"private": true,
"type": "module",
"scripts": {
"dev": "rspack serve --mode development",
"build": "rspack build --mode production"
},
"dependencies": {
"@srcset/react": "*",
"@srcset/runtime": "*"
},
"devDependencies": {
"@rspack/cli": "^2.1.0",
"@rspack/core": "^2.1.4",
"@rspack/dev-server": "^2.2.0",
"@srcset/loader": "*",
"react": "^19.2.0",
"react-dom": "^19.2.0"
}
}
69 changes: 69 additions & 0 deletions examples/rspack/rspack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { rspack } from '@rspack/core'

export default {
cache: {
type: 'persistent'
},
context: import.meta.dirname,
entry: './src/index.jsx',
output: {
path: `${import.meta.dirname}/dist`,
clean: true
},
module: {
rules: [
{
// The app code is JSX, and the workspace packages resolve to their
// TypeScript sources in this repo - one rule covers both, swc infers
// the syntax from the extension. An installed package ships compiled
// JS and needs none of this.
test: /\.[jt]sx?$/,
loader: 'builtin:swc-loader',
options: {
detectSyntax: 'auto',
jsc: {
transform: {
react: {
// No `import React` needed in the components.
runtime: 'automatic'
}
}
}
}
},
{
test: /\.(jpe?g|png|gif)$/i,
use: {
loader: '@srcset/loader',
options: {
rules: [
// Png: keep png as the fallback so transparency survives, add webp.
{
match: '**/*.png',
width: [1, 0.5],
format: ['png', 'webp']
},
// Gif: keep gif as the fallback, add animated webp.
{
match: '**/*.gif',
width: [1, 0.5],
format: ['gif', 'webp']
},
// Everything else - photos. The import query of the first demo overrides this rule.
{
width: [1, 0.5],
format: ['jpg', 'webp', 'avif']
}
]
}
}
}
]
},
plugins: [
// Rspack's built-in html plugin - no html-webpack-plugin needed.
new rspack.HtmlRspackPlugin({
template: './index.html'
})
]
}
99 changes: 99 additions & 0 deletions examples/rspack/src/App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {
Image,
Picture
} from '@srcset/react'
// The rule lives in the import query and overrides the rules from the loader options.
import * as miguelImage from './images/miguel.jpg?{ "width": [1, 0.5, 0.25], "format": ["avif", "webp", "jpg"] }'
// `?placeholder` adds the blur-up data-url export for this import only.
import * as felixImage from './images/felix.jpg?placeholder'
// Both use the matching rules from the loader options.
import * as miguelSticker from './images/miguel-sticker.png'
import * as animation from './images/animation.gif'
import { DemoSection } from './DemoSection.jsx'

// The page is a single column, so one `sizes` fits every demo.
const sizes = '(max-width: 46rem) 100vw, 46rem'

/**
* The demo page: four sections built from `Picture` and `Image`.
* @returns Sections of the page.
*/
export function App() {
return (
<>
{/* 1. Widths and formats from the import query, rendered as a `<picture>`. */}
<DemoSection
title='1. Widths and formats from the import query'
caption='miguel.jpg with the rule { width: [1, 0.5, 0.25], format: ["avif", "webp", "jpg"] } in the import query: Picture renders one <source> per format, ordered avif, webp, jpg by format efficiency.'
srcSet={miguelImage.srcSet}
>
<Picture
srcSet={miguelImage.srcSet}
sizes={sizes}
>
<Image
alt='Miguel the cat, in three widths and three formats'
src={miguelImage.src}
srcSet={miguelImage.srcSet}
sizes={sizes}
/>
</Picture>
</DemoSection>

{/* 2. Blur-up placeholder, and the only place where `priority` is correct. */}
<DemoSection
title='2. Blur-up placeholder and priority'
caption='felix.jpg imported with ?placeholder: Image shows the tiny data-url as its background until the full image loads. `priority` makes it eager, high priority and preloaded - correct here only because this Image is not inside a Picture.'
srcSet={felixImage.srcSet}
>
<Image
alt='Felix the cat, with a blur-up placeholder'
src={felixImage.src}
srcSet={felixImage.srcSet}
placeholder={felixImage.placeholder}
sizes={sizes}
priority
/>
</DemoSection>

{/* 3. Png keeps transparency, webp is added next to it. */}
<DemoSection
title='3. Transparency survives'
caption='miguel-sticker.png with the rule { format: ["png", "webp"] } from the loader options: both formats keep the alpha channel, so the checkerboard behind the sticker shows through.'
srcSet={miguelSticker.srcSet}
>
<Picture
className='checkerboard'
srcSet={miguelSticker.srcSet}
sizes={sizes}
>
<Image
alt='Miguel sticker with a transparent background'
src={miguelSticker.src}
srcSet={miguelSticker.srcSet}
sizes={sizes}
/>
</Picture>
</DemoSection>

{/* 4. Gif keeps every frame, animated webp is added next to it. */}
<DemoSection
title='4. Animation survives'
caption='animation.gif with the rule { format: ["gif", "webp"] } from the loader options: all 50 frames are kept in both the gif and the animated webp variants, so the animation plays.'
srcSet={animation.srcSet}
>
<Picture
srcSet={animation.srcSet}
sizes={sizes}
>
<Image
alt='Animated image'
src={animation.src}
srcSet={animation.srcSet}
sizes={sizes}
/>
</Picture>
</DemoSection>
</>
)
}
42 changes: 42 additions & 0 deletions examples/rspack/src/DemoSection.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* List of the generated variants: id, width and url of every one.
* @param props - Component props.
* @param props.srcSet - All generated variants.
* @returns List element.
*/
function VariantList({ srcSet }) {
return (
<ul className='variants'>
{srcSet.map(({ id, width, url }) => (
<li key={id}>
{id} - {width}w - {url}
</li>
))}
</ul>
)
}

/**
* Demo section: a heading, the images, a caption and the variant list.
* @param props - Component props.
* @param props.title - Section title.
* @param props.caption - Caption text under the images.
* @param props.srcSet - All generated variants, printed under the caption.
* @param props.children - Images to show.
* @returns Section element.
*/
export function DemoSection({
title,
caption,
srcSet,
children
}) {
return (
<section>
<h2>{title}</h2>
{children}
<p className='caption'>{caption}</p>
<VariantList srcSet={srcSet} />
</section>
)
}
Binary file added examples/rspack/src/images/animation.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/rspack/src/images/felix.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/rspack/src/images/miguel-sticker.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/rspack/src/images/miguel.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions examples/rspack/src/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createRoot } from 'react-dom/client'
import { App } from './App.jsx'

createRoot(document.getElementById('app')).render(<App />)
Loading