This chapter contains my attempt to reproduce and learn from Nicolo Ribaudo's talk at HolyJS 2019.
There are several branches in this repository that correspond to the different stages of the talk. The branches are named after the time in the video where Nicolo is explaining code leading to something similar to those that you can find in the files
/src/nicolo-howto-talk/optionalchaining-plugin.cjs
and/src/nicolo-howto-talk/optionalchaining-plugin2.cjs
/src/nicolo-howto-talk/input-*.js
/src/nicolo-howto-talk/loose.config.js
For example, the branch 40m24s
corresponds to the code shown starting at minute 40:24 of the video and corresponds o the section
Loose mode.
nicolo-howto-talk git:(40m24s) git -P branch
29m47s # template.expression.ast
31m14s # The undefined problem
31m31s
34m08s
* 40m24s
main
The target is to build a Babel plugin that transforms the optional chaining proposal obj?.prop
(now a part of the JavaScript language) into a sequence of tests and assignments that check if the object and its properties are defined. To know more about the proposal see
Optional Chaining Proposal.
See folder src/nicolo-howto-talk/production-example and the file /src/nicolo-howto-talk/production-example/README.md for an input example and the output using the current production plugin (2024).
Nicolo starts using an editor that resembles https://astexplorer.net, but it is not clear which one he is using. I will initially go with the AST Explorer and later with VSCode (see folder /src/nicolo-howto-talk).
In the returned object it introduces the manipulateOptions method that is used to modify the behavior of the parser. A plugin could manipulate the parser options using manipulateOptions(opts, parserOpts)
and adding plugins to parserOpts.plugins
. Unfortunately, parser plugins are not real plugins: they are just a way to enable syntax features already implemented in the Babel parser. It is almost impossible to
create a JavaScript parser that adheres to the Open-Closed Principle.
At 26:44 Nicolo has this preliminary code for the plugin:
module.exports = function myPlugin({types: t, template}, options) {
return {
name: "optional-chaining-plugin",
manipulateOptions(opts) {
opts.parserOpts.plugins.push("OptionalChaining")
},
visitor: {
OptionalMemberExpression(path) {
}
}
}
}
At this point we need to review the properties of an OptionalMemberExpression
node: object
, property
, computed
and optional
.
See section optional-property.md for a Explanation of the optional
Property in an OptionalMemberExpression
node in a Babel AST. In section optional-chain.md we compare the Babel and Espree ASTs for obj?.foo.bar
.
At minute 29:40 he has filled the OptionalMemberExpression
visitor with the following code:
module.exports = function myPlugin({types: t, template}, options) {
return {
name: "optional-chaining-plugin",
manipulateOptions(opts) {
opts.parserOpts.plugins.push("OptionalChaining")
},
visitor: {
OptionalMemberExpression(path) {
let { object, property} = path.node;
let memberExp = t.memberExpression(object, property);
path.replaceWith(
template.expression.ast`${object} == null? undefined : ${memberExp}`
)
}
}
}
}
that for an input like obj?.foo
will produce the output:
obj == null ? undefined : obj.foo;
The template.expression
, template.statements
, are variants of the
template
function.
By default @babel/template
returns a function which is invoked with an optional object of replacements, but when using .ast
as in this example, the AST is returned directly.
Notice that you write the code but are interpolating the object
and memberExp
variables which contain
ASTs using ordinary JS backquotes!
... But the code
template.expression.ast`${object} == null? undefined : ${memberExp}`it has a few problems. Someone could write this in their code:
➜ nicolo-howto-talk git:(main) ✗ cat redefine-undefined.cjs
var undefined = 42;
console.log(undefined); // 42
➜ nicolo-howto-talk git:(main) ✗ node redefine-undefined.cjs
42
We have to cope with this kind of bad code and have access to the original undefined
.
The expression void 0 always returns undefined
and we are going to use it instead.
Let us switch from astexplorer to VSCode:
➜ nicolo-howto-talk git:(main) cat
input.js
a?.b
➜ nicolo-howto-talk git:(main) cat babel.config.json
{
"plugins": [
"./optionalchaining-plugin.cjs"
]
}
At minute 29:47 Nicolo uses path.scope.buildUndefined()
to produce void 0
to ensure that undefined
is undefined
:
➜ nicolo-howto-talk git:(main) cat
optionalchaining-plugin.cjs
module.exports = function myPlugin(babel, options) {
const {types: t, template } = babel;
return {
name: "optional-chaining-plugin",
manipulateOptions(opts) {
opts.parserOpts.plugins.push("OptionalChaining")
},
visitor: {
OptionalMemberExpression(path) {
let { object, property} = path.node;
let memberExp = t.memberExpression(object, property);
let undef = path.scope.buildUndefinedNode(); // Create a "void 0" nodes
path.replaceWith(
template.expression.ast`
${object} == null? ${undef} : // Use the "void 0" node
${memberExp}
`
)
}
}
}
}
Execution:
➜ babel-learning git:(29m47s) npx babel src/nicolo-howto-talk/input.js --plugins=./src/nicolo-howto-talk/optionalchaining-plugin.cjs
"use strict";
a == null ? void 0 : a.b;
At minute 31.14 Nicolo considers the more general case of accessing a computed property like in:
➜ nicolo-howto-talk git:(main) ✗ cat input-array.js
a?.[0]
When we feed this input to the plugin we get the output:
➜ nicolo-howto-talk git:(main) ✗ npx babel input-array.js
TypeError: /Users/casianorodriguezleon/campus-virtual/2324/learning/babel-learning/src/nicolo-howto-talk/input-array.js:
Property property of MemberExpression expected node to be
of a type ["Identifier","PrivateName"] but instead got "NumericLiteral"
This is because the property
of the OptionalMemberExpression
is in this case a NumericLiteral
:
➜ nicolo-howto-talk git:(main) compast -blp 'a?.[0]' | yq '.program.body[0]'
{
"type": "ExpressionStatement",
"expression": {
"type": "OptionalMemberExpression",
"object": {
"type": "Identifier",
"name": "a"
},
"computed": true,
"property": {
"type": "NumericLiteral",
"extra": {
"rawValue": 0,
"raw": "0"
},
"value": 0
},
"optional": true
}
}
The error is caused due to the fact that by default value for MemberExpression
s the computed
property is false
and since in the previous code we haven't specified it, it is assumed to be false
. The consequence being that the property
is expected to be an Identifier
or a PrivateName and not a NumericLiteral
.
To avoid the error we take the computed
property of the node and pass it to the t.memberExpression
we build
for the replacement:
➜ babel-learning git:(main) ✗ cat src/nicolo-howto-talk/optionalchaining-plugin.cjs
//const generate = require('@babel/generator').default;
module.exports = function myPlugin(babel, options) {
const {types: t, template } = babel;
return {
name: "optional-chaining-plugin",
manipulateOptions(opts) {
opts.parserOpts.plugins.push("OptionalChaining")
},
visitor: {
OptionalMemberExpression(path) {
let { object, property, computed} = path.node; // <= computed is defined from the node
let memberExp = t.memberExpression(object, property, computed);
let undef = path.scope.buildUndefinedNode();
path.replaceWith(
template.expression.ast`
${object} == null? ${undef} :
${memberExp}
`
)
}
}
}
}
Now the plugin works for both cases a?.b
and a?.[0]
:
➜ babel-learning git:(31m14s) npx babel src/nicolo-howto-talk/input-array.js --plugins=./src/nicolo-howto-talk/optionalchaining-plugin.cjs
"use strict";
a == null ? void 0 : a[0];
At minute 31:31 Nicolo considers the case of the object part being a call expression like a()?.x
.
... As you can see there is a problem, while in the input code
a()
is called once, in the output code it is called twice. Once to check if it isnull
and once to access the property. We can avoid it by storing the result of the call in a variable and then using the variable in thealternate
part of theconditional
expression.
To do that Babel provides the
path.scope.generateUidIdentifier
method that generates a unique identifier that can be used to store the result of the call expression. To declare that variable we use thepath.scope.push
method.
The path.scope.push
method in Babel.js is used to add a new binding (variable) to the current scope. This method is part of the Babel API for manipulating the Abstract Syntax Tree (AST) and is particularly useful when you are developing Babel plugins or transforms and need to introduce new variables into the code.
➜ babel-learning git:(main) ✗ cat src/nicolo-howto-talk/optionalchaining-plugin.cjs
module.exports = function myPlugin(babel, options) {
const {types: t, template } = babel;
return {
name: "optional-chaining-plugin",
manipulateOptions(opts) {
opts.parserOpts.plugins.push("OptionalChaining")
},
visitor: {
OptionalMemberExpression(path) {
let { object, property, computed} = path.node;
let tmp = path.scope.generateUidIdentifier('_obj'); // <= Generate a unique identifier
path.scope.push({id: tmp, kind: "let", init: t.nullLiteral()}); // <= Add the new variable to the scope
let memberExp = t.memberExpression(tmp, property, computed); // <= Use the new variable as Substitute for the object to avoid calling it twice
let undef = path.scope.buildUndefinedNode(); // Safe undefined
path.replaceWith(
template.expression.ast`
${tmp} = ${object} == null? ${undef} :
${memberExp}
`
)
}
}
}
}
Now the plugin works for the case a()?.x
(see the input file at src/nicolohowto-talk/input-function-object.js):
➜ babel-learning git:(31m31s) npx babel src/nicolo-howto-talk/input-function-object.js --plugins=./src/nicolo-howto-talk/optionalchaining-plugin.cjs
"use strict";
let _obj = null;
_obj = a() == null ? void 0 : _obj.x;
At minute 34:08 Nicolo considers
But what if we have more than one nested property?
This is the case of a larger chain of optional properties like a?.x.y.z
whose Babel AST is like follows:
➜ babel-learning git:(34m08s) ✗ compast -blp 'a?.x.y.z' | yq '.program.body[0].expression'
{
"type": "OptionalMemberExpression",
"object": {
"type": "OptionalMemberExpression",
"object": {
"type": "OptionalMemberExpression",
"object": {
"type": "Identifier",
"name": "a"
},
"computed": false,
"property": {
"type": "Identifier",
"name": "x"
},
"optional": true
},
"computed": false,
"property": {
"type": "Identifier",
"name": "y"
},
"optional": false
},
"computed": false,
"property": {
"type": "Identifier",
"name": "z"
},
"optional": false
}
The chaining a?.x.y.z
is actually interpreted as ((a?.x).y).z)
.
We can see that not only the inner a?.x
is
an OptionalMemberExpression
but also
the outer node of (a?.x).y
is an OptionalMemberExpression
.
The difference is that the optional
property of the a?.x
is true
and the optional
property of the (a?.x).y
is false
.
In the same way the a?.x.y.z
is an OptionalMemberExpression
but the optional
property is false
.
If we change the last dot to a?.x.y?.z
then the outer node of (a?.x.y)?.z
is an OptionalMemberExpression
with optional
property set to true
.
Here is again the AST depicted as a graph. OME
stands for OptionalMemberExpression
and we use true
and false
to indicate the optional
property of the nodes:
graph TB
A --> C(("OME<br/>false"))
A((OME<br/>false)) --> B((ID z))
C --> D((OME<br/>true))
C --> E((ID y))
D --> F((Id a))
D --> G((ID x))
We are checking if
a
is nullish. If it is notnullish
we wanto to getx.y.z
. We are not checking if those things are nullish. Otherwise we have had other question marks like thisa?.x?.y.z
.We are currently visiting this starting from the outermost node to the innermost nodes but we should only check transform (the nodes) where the
optional
property istrue
. So we can go down in the AST until we found the "real"optional
property.
Here is a solution slightly different from the one Nicolo proposes:
➜ babel-learning git:(main) ✗ cat src/nicolo-howto-talk/optionalchaining-plugin.cjs
module.exports = function myPlugin(babel, options) {
const { types: t, template } = babel;
return {
name: "optional-chaining-plugin",
manipulateOptions(opts) {
opts.parserOpts.plugins.push("OptionalChaining")
},
visitor: {
OptionalMemberExpression(path) {
while (!path.node.optional) path = path.get("object"); // <= Go down in the AST until we find the "real" optional property
let { object, property, computed } = path.node;
let tmp = path.scope.generateUidIdentifierBasedOnNode(property); // <= Generate a unique identifier based on the property
path.scope.push({ id: tmp, kind: 'let', init: t.NullLiteral() }); // <= Add the new variable to the scope
let memberExp = t.memberExpression(tmp, property, computed);
let undef = path.scope.buildUndefinedNode();
path.replaceWith( // <= Replace the node with the new code
template.expression.ast`
(${tmp} = ${object}) == null? ${undef} :
${memberExp}
`
)
}
}
}
}
Let us consider the following input:
➜ babel-learning git:(main) cat src/nicolo-howto-talk/input-multiple.js
let a = {x: {y: {z: 1}}};
console.log(a?.x.y?.z)
console.log(a?.x.w?.z)
console.log(a?.x.y.z)
when we run the plugin we get the following output:
➜ babel-learning git:(main) npx babel src/nicolo-howto-talk/input-multiple.js --plugins=./src/nicolo-howto-talk/optionalchaining-plugin.cjs
"use strict";
let _z = null, _x = null, _z2 = null, _x2 = null, _x3 = null;
let a = { x: { y: { z: 1 } } };
console.log((_z = ((_x = a) == null ? void 0 : _x.x).y) == null ? void 0 : _z.z);
console.log((_z2 = ((_x2 = a) == null ? void 0 : _x2.x).w) == null ? void 0 : _z2.z);
console.log(((_x3 = a) == null ? void 0 : _x3.x).y.z);
Let us consider the first expression a?.x.y?.z
. We can analyze the translation of ((a?.x).y)?.z
:
(_x = a) == null ? void 0 : _x.x)
is the transformation ofa?.x
. Notice the introduction of the_x
variable.(_z = ((_x = a) == null ? void 0 : _x.x).y)
is the transformation ofa?.x.y
. Notice the introduction of the_z
variableconsole.log((_z = ((_x = a) == null ? void 0 : _x.x).y) == null ? void 0 : _z.z);
is the transformation ofa?.x.y?.z
.
If we pipe the output to node
we get:
➜ babel-learning git:(main) npx babel src/nicolo-howto-talk/input-multiple.js --plugins=./src/nicolo-howto-talk/optionalchaining-plugin.cjs | node
1
undefined
1
You can also check it against the example src/nicolo-howto-talk/input-array.js
➜ babel-learning git:(main) cat src/nicolo-howto-talk/input-array.js
const a = [[2,3]];
console.log(a?.[0][1]);
console.log(a?.[0]?.[2]);
console.log(a?.[0][1]?.[0]);
➜ babel-learning git:(main) npx babel src/nicolo-howto-talk/input-array.js --plugins=./src/nicolo-howto-talk/optionalchaining-plugin.cjs
"use strict";
let _ = null, _2 = null, _3 = null, _4 = null, _5 = null;
const a = [[2, 3]];
console.log(((_ = a) == null ? void 0 : _[0])[1]);
console.log((_2 = (_3 = a) == null ? void 0 : _3[0]) == null ? void 0 : _2[2]);
console.log((_4 = ((_5 = a) == null ? void 0 : _5[0])[1]) == null ? void 0 : _4[0]);
When we run it with node
we get:
➜ babel-learning git:(main) npx babel src/nicolo-howto-talk/input-array.js --plugins=./src/nicolo-howto-talk/optionalchaining-plugin.cjs | node
3
undefined
undefined
The traversing for the first true optional
property can be removed if we visit the OptionalMemberExpression
nodes
in exit
order instead of enter
order. See the solution at /src/nicolo-howto-talk/optionalchaining-plugin2.cjs:
➜ nicolo-howto-talk git:(main) ✗ cat optionalchaining-plugin2.cjs
module.exports = function myPlugin(babel, options) {
const { types: t, template } = babel;
return {
name: "optional-chaining-plugin",
manipulateOptions(opts) {
opts.parserOpts.plugins.push("OptionalChaining")
},
visitor: {
OptionalMemberExpression: {
exit(path) { // <= Now we substitute the "while (!path.node.optional) ..." with a simple return
if (!path.node?.optional) return;
let { object, property, computed } = path.node;
let tmp = path.scope.generateUidIdentifierBasedOnNode(property);
path.scope.push({ id: tmp, kind: 'let', init: t.NullLiteral() });
let memberExp = t.memberExpression(tmp, property, computed);
let undef = path.scope.buildUndefinedNode();
path.replaceWith(
template.expression.ast`
(${tmp} = ${object}) == null? ${undef} :
${memberExp}
`
)
}
}
}
}
}
At minute 40:24 Nicolo introduces loose
mode.
Babel.js "loose mode" is an option that you can enable for certain plugins and presets to generate code that is simpler and potentially more performant but may not strictly adhere to the ECMAScript specification in all edge cases. This mode typically results in output that is closer to how developers might write code manually and can be more efficient in terms of performance and code size.
There are multiple reasons why we want to use "Loose Mode":
- Performance: Loose mode often generates code that executes faster.
- Size: The output code is usually smaller, which can be beneficial for reducing bundle sizes in web applications.
- Simplicity: The generated code is often simpler and easier to understand.
But we have to be aware of the following drawbacks:
- Spec Compliance: The generated code might not fully adhere to the ECMAScript specification, especially in less common edge cases.
- Compatibility: While the generated code works in most cases, there might be subtle differences in behavior compared to the spec-compliant version, which can lead to bugs if not carefully considered.
We can enable loose mode by setting the loose
option to true
in the configuration for specific plugins or presets. Review section Passing plugin options to the visitor methods.
In a Babel plugin, the visitor receives a second parameter after the path
parameter which is usually known as the state
parameter. This state
object that holds any kind of data that the plugin might need to maintain state across the visit. Namely, the property state.opts
contains the options passed to the plugin via the configuration file. We create the following
configuration file:
➜ nicolo-howto-talk git:(40m24s) cat loose.config.js
const path = require('path');
module.exports = {
plugins: [
[ path.join(__dirname, 'optionalchaining-plugin2.cjs'), { loose: true} ],
]
}
Having in mind that we will introduce a conditional in terms of the option:
@@ -8,7 +8,11 @@ module.exports = function myPlugin(babel, options) {
},
visitor: {
OptionalMemberExpression: {
- exit(path) {
+ exit(path, state) {
+ const loose = state.opts.loose;
+ if (loose) {
+ console.log('loose', loose);
+ }
if (!path.node?.optional) return;
let { object, property, computed } = path.node;
➜ nicolo-howto-talk git:(40m24s) ✗ npx babel input.js --config-file ./loose.config.js
loose true
let _b = null;
(_b = a) == null ? void 0 : _b.b;
➜ nicolo-howto-talk git:(40m24s) npx babel input.js --plugins=./optionalchaining-plugin2.cjs
let _b = null;
(_b = a) == null ? void 0 : _b.b;
We are going to use the loose mode to translate an input like a?.x?.[0]
onto a && a.x && a.x[0]
which most of the time
is the same but not always:
> a = { x: [4]}, y = 0
> a?.x?.[0] // 4
> a && a.x && a.x[0] // 4 // the same
> a?.y?.[2] // undefined
> a && a.y && a.y[2] // undefined // the same
> false.toString() // 'false'
> false && false.toString() // false // Not the same!
Here is the new code for the plugin:
➜ nicolo-howto-talk git:(40m24s) cat optionalchaining-plugin2.cjs
module.exports = function myPlugin(babel, options) {
const { types: t, template } = babel;
return {
name: "optional-chaining-plugin",
manipulateOptions(opts) {
opts.parserOpts.plugins.push("OptionalChaining")
},
visitor: {
OptionalMemberExpression: {
exit(path, state) { // Receive the state parameter
if (!path.node?.optional) return;
let { object, property, computed } = path.node;
let tmp = path.scope.generateUidIdentifierBasedOnNode(property);
path.scope.push({ id: tmp, kind: 'let', init: t.NullLiteral() });
let memberExp = t.memberExpression(tmp, property, computed);
if (state?.opts?.loose) { // <= Check the loose option if so make the new translation and return
return path.replaceWith(template.expression.ast`(${tmp} = ${object}) && ${memberExp}`)
}
let undef = path.scope.buildUndefinedNode();
path.replaceWith(
template.expression.ast`
(${tmp} = ${object}) == null? ${undef} :
${memberExp}
`
)
}
}
}
}
}
Now if we run the plugin with the loose mode enabled we get:
➜ nicolo-howto-talk git:(40m24s) cat input-multiple.js
let a = {x: {y: {z: 1}}};
console.log(a?.x.y?.z)
console.log(a?.x.w?.z)
console.log(a?.x.y.z)
➜ nicolo-howto-talk git:(40m24s) npx babel input-multiple.js --config-file ./loose.config.js
let _x = null, _z = null, _x2 = null, _z2 = null, _x3 = null;
let a = { x: { y: { z: 1 } } };
console.log((_z = ((_x = a) && _x.x).y) && _z.z);
console.log((_z2 = ((_x2 = a) && _x2.x).w) && _z2.z);
console.log(((_x3 = a) && _x3.x).y.z);
We can see that the translation of a?.x.y
is (_x = a) && _x.x).y
and for the last optional chain
(a?.x.y)?.z
we add the _z
variable and the && _z.z
suffix:
➜ nicolo-howto-talk git:(40m24s) ✗ npx babel input-multiple.js --config-file ./loose.config.js | node
1
undefined
1
At minute 44.50 Nicolo introduces the subject of testing Babel plugins.
➜ babel-learning git:(44m.50s) npm i -D babel-plugin-tester jest
For more information about the package, see the docs at GitHub: babel-plugin-tester.
Here is the structure of the folder src/nicolo-howto-talk
in this repository:
➜ babel-learning git:(main) tree src/nicolo-howto-talk
src/nicolo-howto-talk
├── __test__
│ ├── fixtures
│ │ ├── basic-functionality
│ │ │ ├── code.js // a?.b;
│ │ │ └── output.js // automatically generated running `jest` the first time
│ │ └── nested
│ │ ├── code.js // a?.b.c?.d
│ │ └── output.js // automatically generated running `jest` the first time
│ └── test.js
├── input-array.js
├── input-function-object.js
├── input-multiple.js
├── input.js
├── loose.config.js
├── optionalchaining-plugin.cjs
├── optionalchaining-plugin2.cjs
├── privatename-example.js
├── production-example
│ ├── README.md
│ ├── babel.config.json
│ └── optional-chaining-input.js
└── redefine-undefined.cjs
And here are the contents of the test file test/test.js:
➜ nicolo-howto-talk git:(44m.50s) ✗ cat __test__/test.js
const pluginTester = require('babel-plugin-tester');
const plugin = require('../optionalchaining-plugin2.cjs');
const path = require('path');
pluginTester({
plugin,
fixtures: path.join(__dirname, 'fixtures'),
});
There are many options we can pass to pluginTester
.
An interesting option to pass to pluginTester
is babel
: This is used to provide your own implementation of babel. This is particularly useful if you want to use a different version of babel.
Another is babelOptions
. This is used to configure babel.
The general behavior is this:
- The first time it runs, it creates the output files.
- Then, in the following runs, it compares the output with the expected output.
➜ nicolo-howto-talk git:(44m.50s) ✗ npx jest
PASS __test__/test.js
optional-chaining-plugin fixtures
✓ 1. basic functionality (232 ms)
✓ 2. nested (8 ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.288 s
Ran all test suites.
The option fixtures must be a path to a directory with a structure similar to the following:
fixtures
├── first-test # test title will be: "1. first test"
│ ├── code.js # required
│ └── output.js # required (unless using the `throws` option)
├── second-test # test title will be: "2. second test"
│ ├── .babelrc.js # optional
│ ├── options.json # optional
│ ├── code.ts # required (other file extensions are allowed too)
│ └── output.js # required (unless using the `throws` option)
└── nested
├── options.json # optional
├── third-test # test title will be: "3. nested > third test"
│ ├── code.mjs # required (other file extensions are allowed too)
│ ├── output.js # required (unless using the `throws` option)
│ └── options.js # optional (overrides props in nested/options.json)
└── x-fourth-test # test title will be: "4. nested > x fourth test"
└── exec.js # required (alternative to code/output structure)
If fixtures
is not an absolute path, it will be path.join
'd with the
directory name of filepath, which is an option that defaults to the absolute path of the file that invoked the pluginTester
function.
And it would run four tests, one for each directory in fixtures containing a file starting with "code
" or "exec
".
This file's contents will be used as the source code input into babel at
transform time. Any file extension can be used, even a multi-part extension
(e.g. .test.js
in code.test.js
) as long as the file name starts with
code.
; the expected output file will have the same file extension suffix
(i.e. .js
in code.test.js
) as this file unless changed with the
fixtureOutputExt
option.
After being transformed by babel, the resulting output will have whitespace trimmed, line endings converted, and then get formatted by prettier.
Note that this file cannot appear in the same directory as exec.js
. If
more than one code.*
file exists in a directory, the first one will be used
and the rest will be silently ignored.
This file, if provided, will have its contents compared with babel's output,
which is code.js
transformed by babel and formatted with prettier.
If this file is missing and neither throws
nor exec.js
are being
used, this file will be automatically generated from babel's output.
Additionally, the name and extension of this file can be changed with the
fixtureOutputName
and fixtureOutputExt
options.
Before being compared to babel's output, this file's contents will have whitespace trimmed and line endings converted.
Note that this file cannot appear in the same directory as exec.js
.
This file's contents will be used as the input into babel at transform time just
like the code.js
file. Use this to make advanced assertions on the output.
For each fixture, the contents of the entirely optional options.json
file are
lodash.mergeWith
'd with the options provided to
babel-plugin-tester, with the former taking precedence. Note that arrays will be
concatenated and explicitly undefined values will unset previously defined
values during merging.
For added flexibility, options.json
can be specified as options.js
instead
so long as a JSON object is exported via module.exports
. If both files
exist in the same directory, options.js
will take precedence and
options.json
will be ignored entirely.
Fixtures support deeply nested directory structures as well as shared or "root"
options.json
files. For example, placing an options.json
file in the
fixtures/nested
directory would make its contents the "global configuration"
for all fixtures under fixtures/nested
. That is: each fixture would
lodash.mergeWith
the options provided to
babel-plugin-tester, fixtures/nested/options.json
, and the contents of their
local options.json
file as described above.
What follows are the properties you may use if you provide an options file, all of which are optional:
This is used to configure babel. Properties specified here override
(lodash.mergeWith
) those from the babelOptions
option provided to babel-plugin-tester. Note that arrays will be concatenated
and explicitly undefined values will unset previously defined values during
merging.
This is used to pass options into your plugin at transform time. Properties
specified here override (lodash.mergeWith
) those from the
pluginOptions
option provided to babel-plugin-tester. Note that arrays
will be concatenated and explicitly undefined values will unset previously
defined values during merging.
Unlike with babel-plugin-tester's options, you can safely mix plugin-specific
properties (like pluginOptions
) with preset-specific properties (like
presetOptions
) in your options files.
At minute 51:57 Nicolo answers the question
... missed one part which was writing feature parser, so support the optional chaining, how complex is that?
At minute 56:05 Nicolo answers the question
... One of the complex things when you are writing any real complex transformation plugin is you do not pollute the scope, and that you are not messing with the scope, like any potential conflicts. Are there any additional tooling except the ones you used for life coding? like generating unique identifiers, creating predefined
undefined
and so on, or is this enough?
As Nicolo mention, there is a problem if you include multiple JS files that run in the same scope,
so if we generate a variable, let us say time
in one file and then we transpile another file, we
don't know if there is a variable with the same name in the first file and so there could be a conflict.
A general solution to this is what I explained in my notes on compilers: prefixing all the user code identifiers with a unique prefix, and avoiding such prefix when introducing new compiler identifiers.
- Watch the talk in Youtube: https://youtu.be/UeVq_U5obnE?si=Vl_A49__5zgITvjx
- See the associated repo at GitHub: https://github.com/nicolo-ribaudo/conf-holyjs-moscow-2019,
- Nicolo slides
- The plugin babel-plugin-transform-optional-chaining at GitHub Babel repo and the way it is used
- Web site of the HolyJS 2019 conference: https://holyjs.ru/en/archive/2019%20Moscow/
- Optional Chaining: From Specification to Implementation by Ross Kirsling (TC39 member) in JS Japan Conference. December 2022
- TC39 Agendas: A repo with a folder per year and markdowns 01.md ... for each meeting.
- See for instance the meeting https://github.com/tc39/agendas/blob/main/2017/01.md and see the slides by Gabriel Isenberg (Champion at the time) for the Null Propagation Operator at Google Slides