counterfact icon indicating copy to clipboard operation
counterfact copied to clipboard

Cannot use import.meta.url in _.context.ts

Open jrunning opened this issue 1 year ago • 7 comments

I wanted to load the contents of a JSON file so I put some code like this in my /api/routes/_.context.ts file, as I'm accustomed to in ES modules:

import {createRequire} from 'module';

const require = createRequire(import.meta.url);
const mockResponse = require('../mocks/response.json');

export class Context {
  getResponse() {
    return mockResponse;
  }
}

When I run Counterfact with the usual arguments I get the following error after "Starting REPL, type .help for more info":

Starting REPL, type .help for more info

node:internal/process/promises:389
      new UnhandledPromiseRejection(reason);
      ^

UnhandledPromiseRejection: This error originated either by throwing inside of an async function 
without a catch block, or by rejecting a promise which was not handled with .catch(). The promise 
rejected with the reason "Error: Line 15: Unexpected reserved word".
    at throwUnhandledRejectionsMode (node:internal/process/promises:389:7)
    at processPromiseRejections (node:internal/process/promises:470:17)
    at process.processTicksAndRejections (node:internal/process/task_queues:96:32) {
  code: 'ERR_UNHANDLED_REJECTION'
}

Node.js v22.3.0

The "unexpected reserved word" in question is the import in import.meta.url, which I have confirmed by removing all code except for console.log(import.meta).

However, when I run the _.context.ts file directly (with tsx and adding console.log(mockResponse) at the end), it runs without an error:

$ npx tsx api/routes/_.context.ts
{ hello: 'world' }

I think something is going wrong in Counterfact's module loader, but I'm not sure what.

In the meantime I'm using JSON.parse(fs.readFileSync(...)), which works fine, but is less than optimal, and there may be other uses of import.meta that are blocked by this issue.

Minimal reproduction

I've created a minimal reproduction of the issue here: jrunning/counterfact-repro.

jrunning avatar Oct 17 '24 19:10 jrunning

I've played with the repo and am trying some alternate ways to load the file. This gets closer:

import { readFileSync } from "fs";
let data;
try {
  const fileData = readFileSync("../../test.json", { encoding: "utf-8" });
  console.log("fileData", fileData);
  data = JSON.parse(readFileSync("../../test.json", { encoding: "utf-8" }));
} catch (error) {
  console.error("parse error!", error);
  data = {};
}

But that doesn't seem to find the proper JSON file so I'm still doing something wrong. But something like this might be an alternative to using require.

dethell avatar Oct 17 '24 19:10 dethell

Ok, using an absolute path works. Probably a better way to help the compiled code in .cache find the desired json file.

dethell avatar Oct 17 '24 20:10 dethell

I've discovered that require is already in scope in _.context.ts (even though it's an ES module—confusing) and while require('../../test.json') doesn't work (it looks for ../../test.json.cjs), require.resolve('../../test.json') does work, so I can do the following:

JSON.parse(readFileSync(require.resolve('../../test.json'), 'utf8'));

jrunning avatar Oct 17 '24 20:10 jrunning

I've discovered that require is already in scope in _.context.ts (even though it's an ES module—confusing) and while require('../../test.json') doesn't work (it looks for ../../test.json.cjs), require.resolve('../../test.json') does work, so I can do the following:

JSON.parse(readFileSync(require.resolve('../../test.json'), 'utf8'));

I guess since all the code transpiles down to cjs this is why require is available. Glad this use case works.

dethell avatar Oct 17 '24 20:10 dethell

Nice solution, @jrunning!

I guess since all the code transpiles down to cjs this is why require is available.

Yep! Because there's no API to clear the module cache with ESM, and I wanted to be able to "hot reload" code, I had to compile everything down to CJS. It gets the job done but leaves us with some leaky abstractions like this one.

Would it help if we add a utility function to Counterfact for loading JSON files?

export class Context {
  constructor({ readJson }) {
     this.readJson = readJson;
  }

  getResponse() {
    return this.readJson('../mocks/response.json');
  }
}

pmcelhaney avatar Oct 17 '24 20:10 pmcelhaney

Would it help if we add a utility function to Counterfact for loading JSON files?

It seems like loading data from JSON could be a common use case, but the only data point I have is my own.

jrunning avatar Oct 17 '24 21:10 jrunning

I'm sure it's common enough, just gut-checking the API.

pmcelhaney avatar Oct 18 '24 15:10 pmcelhaney