Skip to content

Commit

Permalink
feat(@suspensive/react-image): initialize (#353)
Browse files Browse the repository at this point in the history
Co-author: @tooooo1 

# Overview

<!--
    A clear and concise description of what this pr is about.
 -->

This package is experimental package to use img tag with React Suspense

## PR Checklist

- [x] I did below actions if need

1. I read the [Contributing
Guide](https://github.com/suspensive/react/blob/main/CONTRIBUTING.md)
2. I added documents and tests.

---------

Co-authored-by: Chung-il Jung <[email protected]>
  • Loading branch information
manudeli and tooooo1 authored Nov 22, 2023
1 parent c21c190 commit eefff5e
Show file tree
Hide file tree
Showing 29 changed files with 612 additions and 83 deletions.
5 changes: 5 additions & 0 deletions .changeset/warm-moons-confess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@suspensive/react-image": patch
---

feat(react-image): initialize @suspensive/react-image
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/bug.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ body:
- "@suspensive/react"
- "@suspensive/react-query"
- "@suspensive/react-await"
- "@suspensive/react-image"
- etc
validations:
required: true
Expand Down
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ body:
- "@suspensive/react"
- "@suspensive/react-query"
- "@suspensive/react-await"
- "@suspensive/react-image"
- etc
validations:
required: true
Expand Down
2 changes: 2 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
- "packages/react-query/**/*"
"@suspensive/react-await":
- "packages/react-await/**/*"
"@suspensive/react-image":
- "packages/react-image/**/*"
"documentation":
- "docs/**/*"
"visualization":
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,7 @@ jobs:
cache-dependency-path: 'pnpm-lock.yaml'
node-version-file: '.nvmrc'
- run: pnpm install --frozen-lockfile
- if: matrix.command == 'test'
run: pnpm exec playwright install
- name: Run commands
run: pnpm ${{ matrix.command }}
1 change: 1 addition & 0 deletions .github/workflows/code-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ jobs:
cache-dependency-path: 'pnpm-lock.yaml'
node-version-file: '.nvmrc'
- run: pnpm install --frozen-lockfile
- run: pnpm exec playwright install
- run: pnpm test
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
Expand Down
7 changes: 7 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ coverage:
suspensive-react-await:
target: 100%
threshold: 30%
suspensive-image:
target: 100%
threshold: 30%

comment:
layout: "header, reach, diff, flags, components"
Expand All @@ -32,3 +35,7 @@ component_management:
name: "@suspensive/react-await"
paths:
- packages/react-await
- component_id: suspensive-react-image
name: "@suspensive/react-image"
paths:
- packages/react-image
1 change: 1 addition & 0 deletions docs/src/pages/docs/_meta.en.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"react": "@suspensive/react",
"react-query": "@suspensive/react-query",
"react-await": "@suspensive/react-await",
"react-image": "@suspensive/react-image",
"---": {
"type": "separator"
},
Expand Down
1 change: 1 addition & 0 deletions docs/src/pages/docs/_meta.ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"react": "@suspensive/react",
"react-query": "@suspensive/react-query",
"react-await": "@suspensive/react-await",
"react-image": "@suspensive/react-image",
"---": {
"type": "separator"
},
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@
"@testing-library/react": "^13.4.0",
"@types/node": "^18.18.9",
"@types/testing-library__jest-dom": "^5.14.9",
"@vitest/coverage-v8": "^0.34.6",
"@vitest/browser": "^0.34.6",
"@vitest/coverage-istanbul": "^0.34.6",
"@vitest/ui": "^0.34.6",
"commitizen": "^4.3.0",
"cz-conventional-changelog": "^3.3.0",
Expand All @@ -73,6 +74,7 @@
"lint-staged": "^15.1.0",
"ms": "3.0.0-canary.1",
"packlint": "^0.2.4",
"playwright": "^1.39.0",
"prettier": "^3.1.0",
"publint": "^0.2.5",
"rimraf": "^5.0.5",
Expand Down
2 changes: 1 addition & 1 deletion packages/react-await/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default defineConfig({
globals: true,
setupFiles: './vitest.setup.ts',
coverage: {
provider: 'v8',
provider: 'istanbul',
},
},
})
6 changes: 6 additions & 0 deletions packages/react-image/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
root: true,
extends: ['@suspensive/eslint-config/react-ts'],
ignorePatterns: ['*.js*', 'dist', 'coverage'],
}
13 changes: 13 additions & 0 deletions packages/react-image/README.ko.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 라이브러리 소개

[![npm version](https://img.shields.io/npm/v/@suspensive/react-image?color=000&labelColor=000&logo=npm&label=)](https://www.npmjs.com/package/@suspensive/react-image)
[![npm](https://img.shields.io/npm/dm/@suspensive/react-image?color=000&labelColor=000)](https://www.npmjs.com/package/@suspensive/react-image)
[![npm bundle size](https://img.shields.io/bundlephobia/minzip/@suspensive/react-image?color=000&labelColor=000)](https://www.npmjs.com/package/@suspensive/react-image)

## 설치하기

@suspensive/react-image 는 npm에 있습니다. 최신 안정버전을 설치하기 위해 아래 커맨드를 실행하세요

```shell npm2yarn
npm install @suspensive/react-image
```
13 changes: 13 additions & 0 deletions packages/react-image/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Introduction

[![npm version](https://img.shields.io/npm/v/@suspensive/react-image?color=000&labelColor=000&logo=npm&label=)](https://www.npmjs.com/package/@suspensive/react-image)
[![npm](https://img.shields.io/npm/dm/@suspensive/react-image?color=000&labelColor=000)](https://www.npmjs.com/package/@suspensive/react-image)
[![npm bundle size](https://img.shields.io/bundlephobia/minzip/@suspensive/react-image?color=000&labelColor=000)](https://www.npmjs.com/package/@suspensive/react-image)

## Installation

@suspensive/react-image lives in npm. To install the latest stable version, run the following command

```shell npm2yarn
npm install @suspensive/react-image
```
76 changes: 76 additions & 0 deletions packages/react-image/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"name": "@suspensive/react-image",
"version": "0.0.0",
"description": "Useful image interfaces for React Suspense",
"keywords": [
"suspensive",
"react"
],
"homepage": "https://suspensive.org",
"bugs": "https://github.com/suspensive/react/issues",
"repository": {
"type": "git",
"url": "https://github.com/suspensive/react.git",
"directory": "packages/react-image"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/manudeli"
},
"license": "MIT",
"author": {
"name": "Jonghyeon Ko",
"email": "[email protected]"
},
"sideEffects": false,
"type": "module",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./package.json": "./package.json"
},
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"src"
],
"scripts": {
"build": "tsup",
"build:watch": "tsup --watch",
"clean": "rimraf ./dist && rimraf ./coverage",
"lint": "eslint \"**/*.ts*\"",
"lint:attw": "attw --pack",
"lint:pub": "publint --strict",
"prepack": "pnpm build",
"test": "vitest run --coverage",
"test:watch": "vitest --ui --coverage.enabled=true",
"type:check": "tsc --noEmit"
},
"dependencies": {
"use-sync-external-store": "^1.2.0"
},
"devDependencies": {
"@suspensive/package-json-name": "workspace:*",
"@types/react": "^18.2.38",
"@types/react-dom": "^18.2.15",
"@types/use-sync-external-store": "^0.0.6",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"peerDependencies": {
"react": "^16.8 || ^17 || ^18"
},
"publishConfig": {
"access": "public"
}
}
12 changes: 12 additions & 0 deletions packages/react-image/src/Image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { ComponentProps } from 'react'
import { forwardRef } from 'react'
import { useLoad } from './Load'

type ImageProps = ComponentProps<'img'>
export const Image = forwardRef<HTMLImageElement, ImageProps>(function Image({ src, ...props }, ref) {
if (typeof src !== 'string') {
throw new Error('Image of @suspensive/react-image requires src')
}
const loaded = useLoad({ src })
return <img ref={ref} src={loaded.src} {...props} />
})
9 changes: 9 additions & 0 deletions packages/react-image/src/Load.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { describe, expect, it } from 'vitest'
import { load } from './Load'

describe('load', () => {
it('should load image by src', async () => {
const loadedImage = await load('src/assets/test.png')
expect(loadedImage.src).toBe('http://localhost:5173/src/assets/test.png')
})
})
102 changes: 102 additions & 0 deletions packages/react-image/src/Load.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { createElement } from 'react'
import type { FunctionComponent } from 'react'
import { useSyncExternalStore } from 'use-sync-external-store/shim'

/**
* Loads an image from the given source URL.
* @param src - The source URL of the image to load.
* @returns A Promise that resolves to the loaded image.
* @throws Will reject the promise if the image fails to load.
*/
export const load = (src: HTMLImageElement['src']) =>
new Promise<HTMLImageElement>((resolve, reject) => {
const image = new Image()
image.onload = () => resolve(image)
image.onerror = () => reject()
image.src = src
})

type LoadSrc = Parameters<typeof load>[0]

type LoadState<TLoadSrc extends LoadSrc> = {
src: TLoadSrc
promise?: Promise<unknown>
error?: unknown
}

type Notify = (...args: unknown[]) => unknown
class LoadClient {
private loadCache = new Map<LoadSrc, LoadState<LoadSrc>>()
private notifiesMap = new Map<LoadSrc, Notify[]>()

attach(src: LoadSrc, notify: Notify) {
const notifies = this.notifiesMap.get(src)
this.notifiesMap.set(src, [...(notifies ?? []), notify])

return {
detach: () => this.detach(src, notify),
}
}

detach(src: LoadSrc, notify: Notify) {
const notifies = this.notifiesMap.get(src)
if (notifies) {
this.notifiesMap.set(
src,
notifies.filter((item) => item !== notify)
)
}
}

load<TLoadSrc extends LoadSrc>(src: TLoadSrc) {
const loadState = this.loadCache.get(src)

if (loadState?.error) {
throw loadState.error
}
if (loadState?.src) {
return loadState as LoadState<TLoadSrc>
}
if (loadState?.promise) {
throw loadState.promise
}

const newLoadState: LoadState<TLoadSrc> = {
src,
promise: load(src)
.then((image) => (newLoadState.src = image.src as TLoadSrc))
.catch(() => (newLoadState.error = `${src}: load error`)),
}

this.loadCache.set(src, newLoadState)
throw newLoadState.promise
}

private notify(src: LoadSrc) {
const notifies = this.notifiesMap.get(src)
if (notifies) {
for (const notify of notifies) {
notify()
}
}
}
}

const loadClient = new LoadClient()

type UseLoadOptions<TLoadSrc extends LoadSrc> = {
src: TLoadSrc
}
export const useLoad = <TLoadSrc extends LoadSrc>(options: UseLoadOptions<TLoadSrc>) =>
useSyncExternalStore(
(onStoreChange) => loadClient.attach(options.src, onStoreChange).detach,
() => loadClient.load<TLoadSrc>(options.src),
() => loadClient.load<TLoadSrc>(options.src)
)

type LoadProps<TLoadSrc extends LoadSrc> = {
src: TLoadSrc
children: FunctionComponent<LoadState<TLoadSrc>>
}
export const Load = <TLoadSrc extends LoadSrc>({ src, children }: LoadProps<TLoadSrc>) =>
createElement(children, useLoad({ src }))
Binary file added packages/react-image/src/assets/test.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/react-image/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Image } from './Image'
7 changes: 7 additions & 0 deletions packages/react-image/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "@suspensive/tsconfig/react-library.json",
"include": ["."],
"compilerOptions": {
"types": ["node", "@testing-library/jest-dom"]
}
}
4 changes: 4 additions & 0 deletions packages/react-image/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { options } from '@suspensive/tsup'
import { defineConfig } from 'tsup'

export default defineConfig(options)
20 changes: 20 additions & 0 deletions packages/react-image/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import packageJsonName from '@suspensive/package-json-name'
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
name: packageJsonName(),
dir: './src',
environment: 'jsdom',
globals: true,
coverage: {
provider: 'istanbul',
},
browser: {
enabled: true,
headless: true,
provider: 'playwright',
name: 'chromium',
},
},
})
2 changes: 1 addition & 1 deletion packages/react-query/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default defineConfig({
globals: true,
setupFiles: './vitest.setup.ts',
coverage: {
provider: 'v8',
provider: 'istanbul',
},
},
})
2 changes: 1 addition & 1 deletion packages/react/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default defineConfig({
globals: true,
setupFiles: './vitest.setup.ts',
coverage: {
provider: 'v8',
provider: 'istanbul',
},
},
})
Loading

0 comments on commit eefff5e

Please sign in to comment.