-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(image): add load to return image (#44)
fix #35 ## Summary <!-- Please summarize your changes. --> <!-- Please link to any applicable information (forum posts, bug reports, etc.). --> I update `load` to make simply itself. and make @fepack/react-image use `load` of @fepack/image as dependency ## Checks <!-- For completed items, change [ ] to [x]. --> <!-- If you leave this checklist empty, your PR will very likely be closed. --> Please check the following: - [x] I have written documents and tests, if needed. --------- Co-authored-by: 정충일 <[email protected]>
- Loading branch information
Showing
17 changed files
with
140 additions
and
201 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
"@fepack/image": minor | ||
"@fepack/react-image": minor | ||
--- | ||
|
||
feat(image): add `LoadClient`, update `load` can return image, export all public apis of @fepack/image in @fepack/react-image |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,7 @@ | |
"$schema": "https://unpkg.com/@changesets/[email protected]/schema.json", | ||
"changelog": "@changesets/cli/changelog", | ||
"commit": false, | ||
"fixed": [], | ||
"fixed": [["@fepack/image", "@fepack/react-image"]], | ||
"linked": [], | ||
"access": "restricted", | ||
"baseBranch": "main", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,6 @@ | ||
/** @type {import('eslint').Linter.Config} */ | ||
module.exports = { | ||
root: true, | ||
extends: [ | ||
"@fepack/eslint-config-ts/typescript", | ||
"@fepack/eslint-config-js/javascript", | ||
], | ||
extends: ["@fepack/eslint-config-ts"], | ||
ignorePatterns: ["*.js*", "dist", "coverage"], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { load } from "./load"; | ||
|
||
export type LoadSrc = Parameters<typeof load>[0]; | ||
|
||
export type LoadState<TLoadSrc extends LoadSrc> = { | ||
src: TLoadSrc; | ||
promise?: Promise<unknown>; | ||
error?: unknown; | ||
}; | ||
|
||
type Notify = (...args: unknown[]) => unknown; | ||
export 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(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,7 @@ | ||
export { checkWebPSupport } from "./checkWebPSupport"; | ||
export { detect } from "./detect"; | ||
export { extractRGBAs } from "./extractRGBAs"; | ||
export { load, type ImageSource } from "./load"; | ||
export { load } from "./load"; | ||
export { LoadClient } from "./LoadClient"; | ||
|
||
export type { LoadSrc, LoadState } from "./LoadClient"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,49 +1,9 @@ | ||
import { afterAll, beforeAll, describe, expect, it } from "vitest"; | ||
import { describe, expect, it } from "vitest"; | ||
import { load } from "."; | ||
|
||
class MockImage { | ||
public src = ""; | ||
|
||
public setAttribute(name: string, value: string) { | ||
this[name] = value; | ||
} | ||
|
||
public constructor() { | ||
MockImage.lastInstance = this; | ||
} | ||
|
||
public static lastInstance: MockImage; | ||
} | ||
|
||
describe("load", () => { | ||
const originalImage = window.Image; | ||
|
||
beforeAll(() => { | ||
window.Image = MockImage as unknown as typeof Image; | ||
}); | ||
|
||
afterAll(() => { | ||
window.Image = originalImage; | ||
}); | ||
|
||
it("should load defaultSrc when webpSrc is not provided", () => { | ||
load([ | ||
{ | ||
defaultSrc: "./images/test.png", | ||
}, | ||
]); | ||
|
||
expect(MockImage.lastInstance.src).toBe("./images/test.png"); | ||
}); | ||
|
||
it("should load webpSrc when provided", () => { | ||
load([ | ||
{ | ||
defaultSrc: "./images/test.png", | ||
webpSrc: "./images/test.webp", | ||
}, | ||
]); | ||
|
||
expect(MockImage.lastInstance.src).toBe("./images/test.webp"); | ||
it("should load image by src", async () => { | ||
const loadedImage = await load("src/images/test.png"); | ||
expect(loadedImage.src).toBe("http://localhost:5173/src/images/test.png"); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,10 @@ | ||
export interface ImageSource { | ||
defaultSrc: string; | ||
webpSrc?: string; | ||
} | ||
|
||
/** | ||
* Loads the given images. If WebP is WebP source is provided, it will load that. Otherwise, it loads the default source. | ||
* @param {ImageSource[]} images - Array of image sources to preload. | ||
* Loads an image from the given source URL and returns a Promise that resolves to the loaded image. | ||
*/ | ||
export const load = (images: ImageSource[]) => { | ||
for (const image of images) { | ||
const imageElement = new Image(); | ||
imageElement.src = image.webpSrc ? image.webpSrc : image.defaultSrc; | ||
} | ||
}; | ||
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; | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,10 @@ | |
"name": "@fepack/react-image", | ||
"version": "0.0.2", | ||
"license": "MIT", | ||
"author": { | ||
"name": "Jonghyeon Ko", | ||
"email": "[email protected]" | ||
}, | ||
"sideEffects": false, | ||
"type": "module", | ||
"exports": { | ||
|
@@ -23,10 +27,6 @@ | |
"dist", | ||
"src" | ||
], | ||
"author": { | ||
"name": "Jonghyeon Ko", | ||
"email": "[email protected]" | ||
}, | ||
"scripts": { | ||
"build": "tsup", | ||
"build:watch": "tsup --watch", | ||
|
@@ -36,6 +36,9 @@ | |
"prepack": "pnpm build", | ||
"type:check": "tsc --noEmit" | ||
}, | ||
"dependencies": { | ||
"@fepack/image": "workspace:*" | ||
}, | ||
"devDependencies": { | ||
"@fepack/eslint-config": "workspace:^", | ||
"@fepack/tsconfig": "workspace:*", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,106 +1,29 @@ | ||
import type { FunctionComponent } from "react"; | ||
import { createElement, useSyncExternalStore } from "react"; | ||
import { LoadClient, type LoadSrc, type LoadState } from "@fepack/image"; | ||
import { | ||
type FunctionComponent, | ||
createElement, | ||
useSyncExternalStore, | ||
} from "react"; | ||
|
||
type ImageSrc = HTMLImageElement["src"]; | ||
const loadClient = new LoadClient(); | ||
|
||
type LoadOptions<TSrc extends ImageSrc> = { src: TSrc }; | ||
type LoadResult<TSrc extends ImageSrc> = { | ||
event: Event; | ||
src: TSrc; | ||
type UseLoadOptions<TLoadSrc extends LoadSrc> = { | ||
src: TLoadSrc; | ||
}; | ||
|
||
export const load = <TSrc extends ImageSrc>( | ||
options: LoadOptions<TSrc>, | ||
): Promise<LoadResult<TSrc>> => { | ||
const image = new Image(); | ||
|
||
return new Promise((resolve, reject) => { | ||
image.onload = (event) => resolve({ event, src: options.src }); | ||
image.onerror = (event) => reject(event); | ||
|
||
image.src = options.src; | ||
}); | ||
}; | ||
|
||
type Notify = (...args: unknown[]) => unknown; | ||
|
||
const loadCache = new Map<ImageSrc, LoadState>(); | ||
const loadClient = new (class LoadClient { | ||
private notifiesMap = new Map<ImageSrc, Notify[]>(); | ||
|
||
public attach<TSrc extends ImageSrc>(src: TSrc, onNotify: Notify) { | ||
const srcNotifies = this.notifiesMap.get(src); | ||
this.notifiesMap.set(src, [...(srcNotifies ?? []), onNotify]); | ||
|
||
const attached = { | ||
detach: () => this.detach(src, onNotify), | ||
}; | ||
return attached; | ||
} | ||
|
||
public detach<TSrc extends ImageSrc>(src: TSrc, onNotify: Notify) { | ||
const srcNotifies = this.notifiesMap.get(src); | ||
|
||
if (srcNotifies) { | ||
this.notifiesMap.set( | ||
src, | ||
srcNotifies.filter((notify) => notify !== onNotify), | ||
); | ||
} | ||
} | ||
|
||
public _load = <TSrc extends ImageSrc>(src: TSrc): { src: TSrc } => { | ||
const loadStateGot = loadCache.get(src); | ||
|
||
if (loadStateGot?.error) { | ||
throw loadStateGot.error; | ||
} | ||
if (loadStateGot?.src) { | ||
return loadStateGot as LoadState<TSrc>; | ||
} | ||
|
||
if (loadStateGot?.promise) { | ||
throw loadStateGot.promise; | ||
} | ||
|
||
const newLoadState: LoadState<TSrc> = { | ||
src, | ||
promise: load({ src }) | ||
.then(() => { | ||
newLoadState.src = src; | ||
}) | ||
.catch((error) => { | ||
newLoadState.error = error; | ||
}), | ||
}; | ||
|
||
loadCache.set(src, newLoadState); | ||
throw newLoadState.promise; | ||
}; | ||
})(); | ||
|
||
type UseLoadOptions<TSrc extends ImageSrc> = { | ||
src: TSrc; | ||
}; | ||
|
||
type LoadState<TSrc extends ImageSrc = ImageSrc> = UseLoadOptions<TSrc> & { | ||
promise?: Promise<unknown>; | ||
error?: unknown; | ||
}; | ||
|
||
export const useLoad = <TSrc extends ImageSrc>( | ||
options: UseLoadOptions<TSrc>, | ||
): LoadState<TSrc> => | ||
export const useLoad = <TLoadSrc extends LoadSrc>( | ||
options: UseLoadOptions<TLoadSrc>, | ||
) => | ||
useSyncExternalStore( | ||
(onStoreChange) => loadClient.attach(options.src, onStoreChange).detach, | ||
() => loadClient._load<TSrc>(options.src), | ||
() => loadClient._load<TSrc>(options.src), | ||
() => loadClient.load<TLoadSrc>(options.src), | ||
() => loadClient.load<TLoadSrc>(options.src), | ||
); | ||
|
||
type LoadProps<TSrc extends ImageSrc> = LoadOptions<TSrc> & { | ||
children: FunctionComponent<LoadState>; | ||
type LoadProps<TLoadSrc extends LoadSrc> = { | ||
src: TLoadSrc; | ||
children: FunctionComponent<LoadState<TLoadSrc>>; | ||
}; | ||
export const Load = <TSrc extends ImageSrc>({ | ||
export const Load = <TLoadSrc extends LoadSrc>({ | ||
src, | ||
children, | ||
}: LoadProps<TSrc>) => createElement(children, useLoad({ src })); | ||
}: LoadProps<TLoadSrc>) => createElement(children, useLoad({ src })); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
export * from "@fepack/image"; | ||
|
||
export { Load, useLoad } from "./Load"; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.