Skip to content

Commit

Permalink
First implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Ayc0 committed Nov 10, 2022
1 parent ca842e8 commit 2130fe2
Show file tree
Hide file tree
Showing 11 changed files with 2,629 additions and 1 deletion.
22 changes: 22 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Node CI

on: [push]

jobs:
build:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [14.x, 16.x, 18.x, 19.x]

steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Install packages
run: yarn install
- name: Run tests
run: yarn test
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
.DS_Store
123 changes: 122 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,122 @@
# parcel-transformer-preval
# parcel-transformer-preval

Pre-evaluate code at build-time

## The problem

You need to do some dynamic stuff, but don’t want to do it at runtime. Or maybe you want to do stuff like read the file system to get a list of files and you can’t do that in the browser.

## Table of Contents

- [parcel-transformer-preval](#parcel-transformer-preval)
- [The problem](#the-problem)
- [Table of Contents](#table-of-contents)
- [Installation](#installation)
- [Usage](#usage)
- [Important notes:](#important-notes)
- [Parcel config](#parcel-config)
- [Simple example](#simple-example)
- [Serialization](#serialization)
- [How is this different from `babel-plugin-preval`?](#how-is-this-different-from-babel-plugin-preval)

## Installation

As this package will be used during the build process with [Parcel](https://parceljs.org/), it needs to live in your `devDependencies`:

```bash
npm install --save-dev parcel-transformer-preval
# or
yarn add -D parcel-transformer-preval
```

## Usage

### Important notes:

1. This is a [Parcel](https://parceljs.org/) plugin.
2. All code run by `preval` is _not_ run in a sandboxed environment.
3. All code _must_ run synchronously.
4. Code that is run by preval is not transpiled so it must run natively in the version of node you’re running. (cannot use ES Modules).
5. All exported values need to be serializable as JSON for better interop between node environment and the runtime.

### Parcel config

In your `.parcelrc` file, you need enable the preval transformer on `*.preval.js` files (it needs to be the 1st one in the list):

```json
{
"extends": "@parcel/config-default",
"transformers": {
"*.preval.js": ["parcel-transformer-preval", "..."]
}
}
```

And now, you’re ready to use it.

### Simple example

Every file imported that ends in `.preval.js` will be evaluated by `Parcel` at build time:

```js
// file.preval.js
module.exports = 1 + 3;
// other-file.js
import result from "./file.preval.js";

// ↓ ↓ ↓ ↓ ↓ ↓

// other-file.js
const result = 4;
```

### Serialization

As mentioned in the [important notes](#important-notes), `preval` uses JSON serialization. So it can only handle:

- numbers
- strings
- arrays
- simple objects

But not:

- sets
- maps
- functions
- classes
- in general complex “objects”

```js
// Not supported
module.exports = new Set();
module.exports = new Map();
module.exports = new Date();
module.exports = class A {};
module.exports = () => {};
module.exports = function () {};
```

## How is this different from `babel-plugin-preval`?

This plugin is heavily inspired by [`babel-plugin-preval`](https://github.com/kentcdodds/babel-plugin-preval). We could even say that this plugin is a port of the babel plugin for Parcel with a few differences:

This plugin doesn’t support:

- template tags: `` preval`module.exports = …`; ``
- import comments: `import x from /* preval */ './something'`
- `preval.require`s: `preval.require('./something')`
- preval file comments: `// @preval`
- exporting a function.

The reason behind the 4 first differences is because when using it in bigger projects, we realized that being able to easily understand in which environment the code will run.

So instead of having to tell mention from another file that the file we’re importing needs to be prevaled (the import comments and `preval.require`), we found that the file name was a better indicator to reviewers.
As the transformer is now only based on the file name, there is no need for `@preval` comments.

Only using the file name allows you to be able to easily configure:

- ESLint
- TypeScript
- Parcel
- and others
31 changes: 31 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "parcel-transformer-preval",
"version": "0.0.0",
"description": "Pre-evaluate code at build-time",
"main": "src/index.js",
"repository": "[email protected]:Ayc0/parcel-transformer-preval.git",
"author": "Ayc0 <[email protected]>",
"license": "MIT",
"dependencies": {
"@parcel/plugin": "^2.8.0",
"require-from-string": "^2.0.2"
},
"files": [
"src"
],
"devDependencies": {
"@parcel/config-default": "^2.8.0",
"@parcel/core": "^2.8.0",
"@parcel/fs": "^2.8.0",
"ava": "^5.0.1",
"parcel-transformer-preval": "link:."
},
"scripts": {
"test": "ava"
},
"ava": {
"files": [
"!tests/stubs"
]
}
}
27 changes: 27 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const { Transformer } = require("@parcel/plugin");
const requireFromString = require("require-from-string");

const prevalTransfomer = new Transformer({
async transform({ asset }) {
const requireCacheKeysBeforeRun = Object.keys(require.cache);
const output = requireFromString(await asset.getCode()); // execute file
const requireCacheKeysAfterRun = Object.keys(require.cache);

// Delete elements that were added to the cache by the script
// This is to ease re-runs in dev mode if the other dependencies have changed
// without having to stop parcel and restart it.
for (const cacheKey of requireCacheKeysAfterRun) {
if (requireCacheKeysBeforeRun.includes(cacheKey)) {
continue;
}
delete require.cache[cacheKey];
}

asset.setCode(`module.exports = ${JSON.stringify(output)}`);
asset.setMap(null); // impossible to relate the new code and the old one

return [asset];
},
});

module.exports = prevalTransfomer;
73 changes: 73 additions & 0 deletions tests/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
const { Parcel, createWorkerFarm } = require("@parcel/core");
const { MemoryFS } = require("@parcel/fs");
const vm = require("node:vm");

const test = require("ava");

const build = async (input) => {
const workerFarm = createWorkerFarm();
const outputFS = new MemoryFS(workerFarm);

let bundler = new Parcel({
entries: input,
config: require.resolve("./stubs/config.json"),
workerFarm,
outputFS,
});

try {
let { bundleGraph } = await bundler.run();

for (let bundle of bundleGraph.getBundles()) {
const code = await outputFS.readFile(bundle.filePath, "utf8");
const context = { module: {} };
vm.runInNewContext(code, context);
return { code, value: context.module.exports };
}
} finally {
await workerFarm.end();
}
};

test.serial("bundles number properly", async (t) => {
const { code, value } = await build(
require.resolve("./stubs/number.preval.js")
);
t.is(true, code.includes("module.exports = 13"));
t.is(13, value);
t.pass();
});

test.serial(
"bundles array properly & can work with native modules",
async (t) => {
const { code, value } = await build(
require.resolve("./stubs/fs-array.preval.js")
);
t.is(false, code.includes("require"));
t.is(false, code.includes("readdirSync"));

t.deepEqual(value, [
"config.json",
"fs-array.preval.js",
"number.preval.js",
"wrapper.js",
]);
t.pass();
}
);

test.serial("bundles when sub files are preval files", async (t) => {
const { value } = await build(require.resolve("./stubs/wrapper.js"));

t.deepEqual(value, {
number: 13,
fsArray: [
"config.json",
"fs-array.preval.js",
"number.preval.js",
"wrapper.js",
],
});
t.pass();
});
6 changes: 6 additions & 0 deletions tests/stubs/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "@parcel/config-default",
"transformers": {
"*.preval.js": ["parcel-transformer-preval", "..."]
}
}
1 change: 1 addition & 0 deletions tests/stubs/fs-array.preval.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require("fs").readdirSync("./tests/stubs");
1 change: 1 addition & 0 deletions tests/stubs/number.preval.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 1 + 12;
7 changes: 7 additions & 0 deletions tests/stubs/wrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import number from "./number.preval";
import fsArray from "./fs-array.preval";

module.exports = {
number,
fsArray,
};
Loading

0 comments on commit 2130fe2

Please sign in to comment.