remix-image
remix-image copied to clipboard
TypeError: g is not a constructor
Describe the bug
When following the docs and getting setup, when I run the dev server I get:
/Users/<app>/node_modules/is-svg/index.js:41
module.exports.default = isSvg;
^
TypeError: g is not a constructor
at new DiskCache (/Users/<app>/node_modules/is-svg/index.js:41:26)
at Object.<anonymous> (/Users/<app>/app/pages/api.image.tsx:16:10)
at Module._compile (node:internal/modules/cjs/loader:1159:14)
at Object.Module._extensions..js (node:internal/modules/cjs/loader:1213:10)
at Module.load (node:internal/modules/cjs/loader:1037:32)
at Function.Module._load (node:internal/modules/cjs/loader:878:12)
at Module.require (node:internal/modules/cjs/loader:1061:19)
at require (node:internal/modules/cjs/helpers:103:18)
at Server.<anonymous> (/Users/<app>/build/server.js:50:3)
at Object.onceWrapper (node:events:627:28)
Your Example Website or App
No response
Steps to Reproduce the Bug or Issue
Just follow the installation docs
Expected behavior
No errors when starting server
Screenshots or Videos
No response
Platform
- OS: macOS
- Browser: Safari
- Version: latest
Additional context
No response
I had this same issue, but solved it by making sure my imports looked like this:
import Image, { MimeType } from "remix-image"
I had some auto-import issues screw me up
@JClackett I am aware of the issue and have been looking into a solution. The cause of this issue is the DiskCache option. (In the meantime, I suggest replacing this with MemoryCache)
Cause:
Remix-Image uses a library called @next-boost/hybrid-disk-cache
for the DiskCache. They currently have an issue on their repo that states the same problem about it not being a constructor, but it seems the library is not in active development.
Fix: I would like to implement our own disk storage adapter for this project. However, it may be a few weeks before I'd have the time to work on this, so I'm open to any PRs in the meantime.
Related:
- https://remix-image.mcfarl.in/docs/advanced/creating-a-cache
- keyv: https://www.npmjs.com/package/keyv (optional helper for some expiration functionality)
- keyv-file: https://github.com/zaaack/keyv-file (unoptimal, stores all image Uint8Array as JSON in a single file. I feel like storing images individually might be the way to go, but we also need to track file sizes and delete files if cache gets too big. Otherwise, this Cache could just use a field called
maxFiles
and not worry about their size.
I had exactly the same issue and in the meantime used your suggestion @Josh-McFarlin .
Any updates or should I just focus on creating my own custom cache?
Could use an update on this as well
For anyone interested I kind of just made my own based on this package that saves to a local folder (fly volumes in my case).
// image.server.ts
import { Response as NodeResponse } from "@remix-run/node"
import type { LoaderArgs } from "@vercel/remix"
import axios from "axios"
import { createHash } from "crypto"
import fs from "fs"
import fsp from "fs/promises"
import path from "path"
import sharp from "sharp"
import { IS_PRODUCTION } from "~/lib/config.server"
const badImageBase64 = "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
function badImageResponse() {
const buffer = Buffer.from(badImageBase64, "base64")
return new Response(buffer, {
status: 500,
headers: {
"Cache-Control": "max-age=0",
"Content-Type": "image/gif;base64",
"Content-Length": buffer.length.toFixed(0),
},
})
}
function getIntOrNull(value: string | null) {
if (!value) return null
return Number.parseInt(value)
}
export async function generateImage({ request }: LoaderArgs) {
try {
const url = new URL(request.url)
const src = url.searchParams.get("src")
if (!src) return badImageResponse()
const width = getIntOrNull(url.searchParams.get("width"))
const height = getIntOrNull(url.searchParams.get("height"))
const quality = getIntOrNull(url.searchParams.get("quality")) || 90
const fit = (url.searchParams.get("fit") as keyof sharp.FitEnum) || "cover"
// Create hash of the url for unique cache key
const hash = createHash("sha256")
.update("v1")
.update(request.method)
.update(request.url)
.update(width?.toString() || "0")
.update(height?.toString() || "0")
.update(quality?.toString() || "80")
.update(fit)
const key = hash.digest("hex")
const cachedFile = path.resolve(path.join(IS_PRODUCTION ? "data/cache/images" : ".data/cache/images", key + ".webp"))
const exists = await fsp
.stat(cachedFile)
.then((s) => s.isFile())
.catch(() => false)
// if image key is in cache return it
if (exists) {
console.log({ Cache: "HIT", src })
const fileStream = fs.createReadStream(cachedFile)
return new NodeResponse(fileStream, {
status: 200,
headers: { "Content-Type": "image/webp", "Cache-Control": "public, max-age=31536000, immutable" },
})
} else {
console.log({ Cache: "MISS", src })
}
// fetch from original source
const res = await axios.get(src, { responseType: "stream" })
if (!res) return badImageResponse()
// transform image
const sharpInstance = sharp()
sharpInstance.on("error", (error) => {
console.error(error)
})
if (width || height) sharpInstance.resize(width, height, { fit })
sharpInstance.webp({ quality })
// save to cache
await fsp.mkdir(path.dirname(cachedFile), { recursive: true }).catch(() => {
// dont need to throw here, just isnt cached in file system
})
const cacheFileStream = fs.createWriteStream(cachedFile)
const imageTransformStream = res.data.pipe(sharpInstance)
await new Promise<void>((resolve) => {
imageTransformStream.pipe(cacheFileStream)
imageTransformStream.on("end", () => {
resolve()
})
imageTransformStream.on("error", async () => {
// remove file if transform fails
await fsp.rm(cachedFile).catch(() => {
// dont need to throw here, just isnt removed from file system
})
})
})
// return transformed image
const fileStream = fs.createReadStream(cachedFile)
return new NodeResponse(fileStream, {
status: 200,
headers: { "Content-Type": "image/webp", "Cache-Control": "public, max-age=31536000, immutable" },
})
} catch (e) {
console.log(e)
return badImageResponse()
}
}
// api.image.ts
import { generateImage } from "~/services/image.server"
export const loader = generateImage
for what its worth, I've created a pure fs disk cache implementation:
import crypto from 'node:crypto'
import { access, writeFile, rmdir, readFile } from 'node:fs/promises'
import { mkdirSync } from 'node:fs'
import type { CacheConfig, CacheStatus } from 'remix-image/server'
import { Cache } from 'remix-image/server'
type DiskCacheConfig = CacheConfig & {
path: string
}
function createHash(input: string) {
const hash = crypto.createHash('sha256')
hash.update(input)
return hash.digest('hex')
}
export class DiskCache extends Cache {
config: DiskCacheConfig
constructor(config: Partial<DiskCacheConfig> | undefined = {}) {
super()
this.config = {
path: config.path ?? '.cache/remix-image',
ttl: config.ttl ?? 24 * 60 * 60,
tbd: config.tbd ?? 365 * 24 * 60 * 60,
}
mkdirSync(this.config.path, { recursive: true })
}
has(key: string): Promise<boolean> {
return access(`${this.config.path}/${createHash(key)}`)
.then(() => true)
.catch(() => false)
}
status(key: string): Promise<CacheStatus> {
// this code never gets called, not sure why.
throw new Error('Method not implemented.')
}
get(key: string): Promise<Uint8Array | null> {
return readFile(`${this.config.path}/${createHash(key)}`).catch(() => null)
}
set(key: string, resultImg: Uint8Array): Promise<void> {
return writeFile(`${this.config.path}/${createHash(key)}`, resultImg)
}
clear(): Promise<void> {
return rmdir(this.config.path, { recursive: true })
}
}
Any updates on this?