kentcdodds / babel-plugin-preval

🐣 Pre-evaluate code at build-time
https://npm.im/babel-plugin-preval
MIT License
1.36k stars 71 forks source link

Simplify function injection #73

Open AndrewRot opened 4 years ago

AndrewRot commented 4 years ago

So I was trying to use preval to insert functions into my code before run time. What I thought was going to be quite trivial, turned out to be a bit tricky. In order to insert functions, preval implicitly requires you to wrap your exports in an object.

Example function:

function doThing(message) { return message + ' a thing'; }
module.exports = { doThing }; // You must wrap it here [see explanation A below]

Usage of preval:

const doThing = preval.require('./doDigest.js').doThing; // pull out function from object
// In order to insert a function, you must wrap it in an object and then extract it

Generated code:

var doThing = {
  "doThing": function doThing(message) {
    return message + ' a thing';
  }
}.doThing;

explanation A

Preval then creates an object with our function inside. This is a bit funky for users. If you attempt to do this without wrapping your exports as an object, preval will fail to inject the function with the following behavior:

confusing failed scenario to insert function

Example function:

function doThing(message) { return message + ' a thing'; }
module.exports = doThing; // Omission of object wrapper

Usage of preval:

const doThing = preval.require('./doDigest.js');

Generated code:

var doThing = "undefined a thing";

Suggestion

A.) To make this easier for users to insert functions into their code, you may want to consider the way preval requires modules. B.) Add an example to the README.md that illustrates how this is done.

kentcdodds commented 4 years ago

This is actually a feature that should be documented. If you export a function, them preval will call that function and whatever you return will be the thing used for replacement. Meaning you could return a function if you want:

function doThing(message) { return message + ' a thing'; }
module.exports = () => doThing;

Would you like to update your pull request to add documentation for this feature?

Maycon-Santos commented 4 years ago

Recently I needed to "generate" a function and thought about the following method:

const getAssets = preval`module.exports = require('./build-time/get-assets')`
const assets = preval`module.exports = { run: () => ({${getAssets}}) }`.run()

getAssets returns something like: require('./image-path-1'); require('./image-path-2'); .... assets puts this string into the function body, generating something like:

{
  run: () => { require('./image-path-1'); require('./image-path-2');  ...}
}

Doubts:

Sorry for my English, I'm a beginner at this.

kentcdodds commented 4 years ago

You should probably be using babel-plugin-codegen which is more powerful.

Hideman85 commented 1 year ago

I have a similar needs, I was trying out this tool and got nicely surprised at beginning but then I just tried to put a condition and then :boom:

Second point super annoying is I would like to preserve my modules from beginning to end and output es2020 code in the end.

Sample used in test I use esbuild to bundle the content.... ```js // lib.ts var factory = (str, num) => ({ str, num }); var builder = () => { const obj = {}; const methods = { str: (str) => { obj.str = str; return methods; }, num: (num) => { obj.num = num; return methods; }, foo: (foo) => { obj.foo = foo; return methods; }, build: () => { return obj; } }; return methods; }; var Builder = class { constructor() { this.obj = {}; } str(str) { this.obj.str = str; return this; } num(num) { this.obj.num = num; return this; } foo(foo) { this.obj.foo = foo; return this; } build() { return this.obj; } }; // test.ts var a = { foo: "bar", ...factory("Hello", 1) }; var b = builder().foo("bar").str("Hello").num(1).build(); var c = new Builder().foo("bar").str("Hello").num(1).build(); var d = (str) => { let cond = new Builder().foo("bar").num(1); if (str) { cond = cond.str(str); } return cond.build(); }; var e = (str) => { let cond = builder().foo("bar").num(1); if (str) { cond = cond.str(str); } return cond.build(); }; module.exports = () => ({ a, b, c, d, e }); ``` ...then I have a simple index file that preval it and exports the content... ```js const { a, b, c, d, e } = preval.require('./test-bundled') export { a, b, c, d, e } ``` ...but then the produced result is invalid. ```js const { a, b, c, d, e } = { "a": { "foo": "bar", "str": "Hello", "num": 1 }, "b": { "foo": "bar", "str": "Hello", "num": 1 }, "c": { "foo": "bar", "str": "Hello", "num": 1 }, "d": str => { let cond = new Builder().foo("bar").num(1); if (str) { cond = cond.str(str); } return cond.build(); }, "e": str => { let cond = builder().foo("bar").num(1); if (str) { cond = cond.str(str); } return cond.build(); } }; export { a, b, c, d, e }; ```

My goal is to have a kind of builder pattern that could be statically replaced. Example:

// Builder declared in the spoiler
export const request = (str) => {
  let cond = builder().foo("bar").num(1);
  if (str) {
    return cond.str(str).build();
  }
  return cond.build();
}

// Statically built as
export const request = (str) => ({
  foo: 'bar',
  num: 1,
  ...(str ? { str } : {}),
})

Of course this is a minimal example but each step would impact several attributes in that object and it would be great to be built as a single object with conditionals spreading for instance.

Hideman85 commented 1 year ago

Any help/news on this?