1

Consider this really simple example:

class MyClass {
  public add(num: number): number {
    return num + 2;
  }
}
const result = await page.evaluate((NewInstance) => {
  console.log("typeof instance", typeof NewInstance); // undefined
  const d = new NewInstance();
  console.log("result", d.add(10));
  return d.add(10);
}, MyClass);

I've tried everything I could think of. The main reason I want to use a class here, is because there's a LOT of code I don't want to just include inside the evaluate method directly. It's messy and hard to keep track of it, so I wanted to move all logic to a class so it's easier to understand what's going on.

Is this possible?

ggorlen
  • 44,755
  • 7
  • 76
  • 106
Shannon Hochkins
  • 11,763
  • 15
  • 62
  • 95
  • You can [put the class in an external file and include a script](https://stackoverflow.com/a/72157472/6243352). But are sure you this isn't an [xy problem](https://meta.stackexchange.com/a/233676/399876)? If you don't mind providing more context for what you're trying to achieve, there may be a better solution. In the meantime, all the typical strategies for serializing classes may be worth looking into: [1](https://stackoverflow.com/questions/40201589), [2](https://stackoverflow.com/questions/38922990), [3](https://stackoverflow.com/questions/51461461) etc. – ggorlen Feb 16 '23 at 07:47

1 Answers1

2

It's possible, but not necessarily great design, depending on what you're trying to do. It's hard to suggest the best solution without knowing the actual use case, so I'll just provide options and let you make the decision.

One approach is to stringify the class (either by hand or with .toString()) or put it in a separate file, then addScriptTag:

const puppeteer = require("puppeteer"); // ^19.6.3

class MyClass {
  add(num) {
    return num + 2;
  }
}

let browser;
(async () => {
  browser = await puppeteer.launch();
  const [page] = await browser.pages();
  await page.goto(
    "https://www.example.com",
    {waitUntil: "domcontentloaded"}
  );
  await page.addScriptTag({content: MyClass.toString()});
  const result = await page.evaluate(() => new MyClass().add(10));
  console.log(result); // => 12
})()
  .catch(err => console.error(err))
  .finally(() => browser?.close());

See this answer for more examples.

Something like eval is also feasible. If it looks scary, consider that anything you put into a page.evaluate() or page.addScriptTag() is effectively the same thing as far as security goes.

const result = await page.evaluate(MyClassStringified => {
  const MyClass = eval(`(${MyClassStringified})`);
  return new MyClass().add(10);
}, MyClass.toString());

Many other patterns are also possible, like exposing your library via exposeFunction if the logic is Node-based rather than browser-based.

That said, defining the class inside an evaluate may not be as bad as you think:

const addTonsOfCode = () => {

MyClass = class {
  add(num) {
    return num + 2;
  }
}

// ... tons of code ...
};

let browser;
(async () => {
  browser = await puppeteer.launch();
  const [page] = await browser.pages();
  await page.goto(
    "https://www.example.com",
    {waitUntil: "domcontentloaded"}
  );
  await page.evaluate(addTonsOfCode);
  const result = await page.evaluate(() => new MyClass().add(10));
  console.log(result); // => 12
})()
  .catch(err => console.error(err))
  .finally(() => browser?.close());

I'd prefer to namespace this all into a library:

const addTonsOfCode = () => {

class MyClass {
  add(num) {
    return num + 2;
  }
}

// ... tons of code ...

window.MyLib = {
  MyClass,
  // ...
};

};

Then use with:

await page.evaluate(addTonsOfCode);
await page.evaluate(() => new MyLib.MyClass().add(10));
ggorlen
  • 44,755
  • 7
  • 76
  • 106
  • 1
    Personally i think the MyClass.toString() is the cleanest method - i know it's similar in nature to eval, but it makes the code more readable and less abstract! Thankyou for this very detailed answer! – Shannon Hochkins Feb 16 '23 at 20:27
  • There's actually a lot of solutions here, none seem to work with typescript & separation, if i want to have all my class code in a separate file, i get import errors with pupeteer, using with .toString() i get __name is undefined because the functions don't have the correct scope – Shannon Hochkins Feb 16 '23 at 20:53
  • Hmm.. Maybe ask a new question with the code as a [mcve]. I'll take a look. – ggorlen Feb 16 '23 at 21:02
  • 1
    Nevermind @ggorlen, yyour first solution using content: MyClass.toString() works when i reference inside evaluate using `window.MyClass` instead of directly :) – Shannon Hochkins Feb 16 '23 at 23:12
  • @ggorlen, How would you go about it when `MyClass` extends `MyBaseClass`. If I use your examples then I get an error like `_myclass.MyBaseClass` is undefined. Perhaps this has something to do with TypeScript but I'm not sure at this point! – Jarno van Rhijn Aug 21 '23 at 13:13
  • @JarnovanRhijn I would ask a new question since this seems like a larger scope than OP is asking about. If you `addScriptTag` or `eval`, it should work regardless of subclassing though. – ggorlen Aug 21 '23 at 13:49