remix-image icon indicating copy to clipboard operation
remix-image copied to clipboard

TypeError: g is not a constructor

Open JClackett opened this issue 2 years ago • 8 comments

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

JClackett avatar Dec 22 '22 11:12 JClackett

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

mrcampbell avatar Dec 25 '22 03:12 mrcampbell

@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.

Josh-McFarlin avatar Dec 25 '22 04:12 Josh-McFarlin

I had exactly the same issue and in the meantime used your suggestion @Josh-McFarlin .

Danones avatar Jan 16 '23 18:01 Danones

Any updates or should I just focus on creating my own custom cache?

TommyBloom avatar Feb 22 '23 19:02 TommyBloom

Could use an update on this as well

chrisbull avatar May 11 '23 12:05 chrisbull

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

JClackett avatar May 11 '23 12:05 JClackett

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 })
	}
}

lifeiscontent avatar May 22 '23 05:05 lifeiscontent

Any updates on this?

andreaspapav avatar Dec 14 '23 13:12 andreaspapav