Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeError: g is not a constructor #62

Open
JClackett opened this issue Dec 22, 2022 · 8 comments · May be fixed by #75
Open

TypeError: g is not a constructor #62

JClackett opened this issue Dec 22, 2022 · 8 comments · May be fixed by #75

Comments

@JClackett
Copy link

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

@mrcampbell
Copy link

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

@Josh-McFarlin
Copy link
Owner

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

@Danones
Copy link

Danones commented Jan 16, 2023

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

@TommyBloom
Copy link

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

@chrisbull
Copy link

Could use an update on this as well

@JClackett
Copy link
Author

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

@lifeiscontent
Copy link

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

Kiyozz pushed a commit to Kiyozz/remix-image that referenced this issue Aug 22, 2023
Add `interop: "auto"` to rollup config and remove terser transformation for remix-image/server

Terser is not useful for nodejs' only package and make debug harder

    BaseCache is not a constructor

is better than

    g is not a constructor

Closes Josh-McFarlin#62
Kiyozz added a commit to Kiyozz/remix-image that referenced this issue Aug 22, 2023
Add `interop: "auto"` to rollup config and remove terser transformation for remix-image/server

Terser is not useful for nodejs' only package and make debug harder

    BaseCache is not a constructor

is better than

    g is not a constructor

Closes Josh-McFarlin#62
Kiyozz added a commit to Kiyozz/remix-image that referenced this issue Aug 22, 2023
Add `interop: "auto"` to rollup config and remove terser transformation for remix-image/server

Terser is not useful for nodejs' only package and make debug harder

    BaseCache is not a constructor

is better than

    g is not a constructor

Closes Josh-McFarlin#62
@andreaspapav
Copy link

Any updates on this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants