Skip to content

Commit

Permalink
feat(image): add load to return image (#44)
Browse files Browse the repository at this point in the history
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
manudeli and tooooo1 authored Sep 24, 2023
1 parent d48f72e commit 36e57de
Show file tree
Hide file tree
Showing 17 changed files with 140 additions and 201 deletions.
6 changes: 6 additions & 0 deletions .changeset/brave-timers-lick.md
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
2 changes: 1 addition & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 1 addition & 4 deletions packages/image/.eslintrc.cjs
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"],
};
65 changes: 65 additions & 0 deletions packages/image/src/LoadClient.ts
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();
}
}
}
10 changes: 1 addition & 9 deletions packages/image/src/extractRGBAs.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
import { describe, expect, it } from "vitest";
import { extractRGBAs } from ".";

const load = (src: HTMLImageElement["src"]) =>
new Promise<HTMLImageElement>((resolve, reject) => {
const image = new Image();
image.onload = () => resolve(image);
image.onerror = () => reject();
image.src = src;
});
import { extractRGBAs, load } from ".";

describe("extractRGBAs", () => {
it("should return an array of colors", async () => {
Expand Down
5 changes: 4 additions & 1 deletion packages/image/src/index.ts
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";
48 changes: 4 additions & 44 deletions packages/image/src/load.spec.ts
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");
});
});
21 changes: 8 additions & 13 deletions packages/image/src/load.ts
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;
});
11 changes: 7 additions & 4 deletions packages/react-image/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -23,10 +27,6 @@
"dist",
"src"
],
"author": {
"name": "Jonghyeon Ko",
"email": "[email protected]"
},
"scripts": {
"build": "tsup",
"build:watch": "tsup --watch",
Expand All @@ -36,6 +36,9 @@
"prepack": "pnpm build",
"type:check": "tsc --noEmit"
},
"dependencies": {
"@fepack/image": "workspace:*"
},
"devDependencies": {
"@fepack/eslint-config": "workspace:^",
"@fepack/tsconfig": "workspace:*",
Expand Down
115 changes: 19 additions & 96 deletions packages/react-image/src/Load.ts
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 }));
2 changes: 2 additions & 0 deletions packages/react-image/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from "@fepack/image";

export { Load, useLoad } from "./Load";
12 changes: 8 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"lint:pub": { "cache": false },
"prepack": {
"outputs": ["dist/**"],
"dependsOn": ["^prepack"],
"cache": false
},
"test": {
Expand Down
Loading

0 comments on commit 36e57de

Please sign in to comment.