Custom babel plugin - cannot traverse (visit) JSXElement - javascript

I am trying to write a custom babel transform plugin. When see the AST for a React component in astxplorer.net, I see JSXElement in the tree as a node.
But when I try to log the path for the visitor JSXElement, nothing logs to the console.
React component
const HelloWorld = () => <span>Hello World</span>
Code to transform the component and log JSXElement while visiting
transform.test.js
const { code, ast } = transformSync(HelloWorld, {
ast: true,
plugins: [
function myOwnPlugin() {
return {
visitor: {
JSXElement(path) {
// nothing logs to the console
console.log("Visiting: " + path);
}
}
};
},
]
});
If it helps, I have the transform.test.js inside an ejected create-react-app project. CRA comes with built-in preset react-app.
"babel": {
"presets": [
"react-app"
]
}
I run the test with the command jest transform.test.js and I have babel-jest transform applied by default in CRA jest setup.

I think you're passing the component to Babel instead of the code for the component, you need to transform the actual code as a string, for example, something like this:
const { code, ast } = transformSync(`const HelloWorld = () => <span>Hello World</span>`, {
ast: true,
plugins: [
function myOwnPlugin() {
return {
visitor: {
JSXElement(path) {
// nothing logs to the console
console.log("Visiting: " + path);
}
}
};
},
]
});```

Related

How to compile template string at build time using Babel (not Webpack)

I am using Babel (without Webpack) to transpile some ES6 code. The code contains a template literal which I would like to evaluate at build time.
The code contains the following import where I would like to inject the version:
const Component = React.lazy(() => import(`projectVersion${__VERSION__}/FooBar`))
I have used a number of different plugins to try and achieve this such as babel-plugin-inline-replace-variables, babel-plugin-transform-inline-environment-variables, but the code compiles to something like this:
var Component = /*#__PURE__*/_react.default.lazy(function () {
return Promise.resolve("projectVersion".concat("1", "/FooBar")).then(function (s) {
return _interopRequireWildcard(require(s));
});
});
But what I'm after is something along the lines of this (as if I just manually added the version to projectVersion):
const Component = /* #__PURE__*/_react.default.lazy(() => Promise.resolve().then(() => _interopRequireWildcard(require('projectVersion1/FooBar'))))
One of the reasons for this is because when running the code with concat, I get the error Critical dependency: the request of a dependency is an expression.
Thanks in advance for any help and suggestions.
More Information
Command: babel src --out-dir build
Babel Config:
module.exports = api => {
api.cache(true)
return {
presets: [
['#babel/preset-env', {
corejs: 3,
targets: '> 0.25%, not dead',
useBuiltIns: 'usage',
}],
['#babel/preset-react', {
runtime: 'automatic',
}],
'#babel/preset-typescript',
],
plugins: [],
}
}
Having tried many different potential solutions, I eventually landed on writing a custom babel plugin to replace the TemplateLiteral with a StringLiteral.
The code below is written quite lazily and is specific for my use-case, but hopefully it will help someone. If I have time, I will make it more robust and re-useable and share it on npm.
module.exports = ({ types: t }) => ({
visitor: {
TemplateLiteral(path, state) {
const { version } = state.opts
const { expressions, quasis } = path.node
const expressionStrings = expressions.map(exp => exp.name)
if (expressionStrings.length === 1 && expressionStrings.includes('__VERSION__')) {
const startString = quasis.find(q => q.tail === false).value.raw
const endString = quasis.find(q => q.tail === true).value.raw
path.replaceWith(
t.StringLiteral(`${startString}${version}${endString}`),
)
}
},
},
})
Using the plugin is as simple as adding it to your babel plugins:
plugins: [
['./babel/interpolate-template-literal', {
version: "99",
}],
],

Babel doesn't update the change in plugins

I am using babel with jest for testing nodejs and instrumentation. The problem I am facing is babel is somehow caching(my guess) the transformed code and doesn't update even after changes in plugins code. The reasoning behind my guess is if I run jest with new test, it shows the change. But after the first time, it shows the same output, even though I have changed the custom plugin code in babel.
This is my babel.config.js
module.exports = function (api) {
const presets = [];
const plugins = [
[require(`./babel-instrumentor.js`)],
[require("#babel/plugin-transform-modules-commonjs").default],
];
/** this is just for minimal working purposes,
* for testing larger applications it is
* advisable to cache the transpiled modules in
* node_modules/.bin/.cache/#babel/register* */
api.cache(false);
return {
presets,
plugins,
};
};
And this is the babel-instrumentor.js
console.log("Loaded babel plugin");
module.exports = function ({ types: t }) {
return {
name: "log-functions",
visitor: {
Function: {
enter(path) {
let name = "anon";
if (path.node.id)
name = path.node.id.name;
console.log(path.node.body);
},
},
},
};
};
I also couldn't find any cache folder in node_modules which is supposed to be in
node_modules/.bin/.cache/#babel/register

Webpack: ModuleNotFoundError for an unimported module

The problem
My project consists of a yarn monorepo in which, among various packages, there is a NextJS app and a configuration package (which is also shared with the server and a react-native application). In the configuration module what I do is to export the production keys if the environment is production, while if the project is under development, the development ones.
import { merge } from "lodash";
import { IConfig, RecursivePartial } from "./interfaces";
import { defaultKeys } from "./default.keys";
import { existsSync } from "fs";
const secretKeys: RecursivePartial<IConfig> = (function (env) {
switch (env) {
case "production":
return require("./production.keys").productionKeys;
default:
try {
if (!existsSync("./development.keys")) {
return require("./production.keys").productionKeys;
} else {
return require("./development.keys").developmentKeys;
}
} catch (e) {
}
}
})(process.env.NODE_ENV);
export const keys = merge(defaultKeys, secretKeys) as IConfig;
Of course, the development configuration file is included in the .gitignore file and therefore does not exist during production.
This has always worked perfectly (with the server and the mobile app), and indeed, there was never the need to check with the fs module if the development.keys file exists (check which I added later).
However, I recently added a NextJs application to the project. I encountered no problems during development, however when I tried to deploy the app on Heroku I encountered this error:
ModuleNotFoundError: Module not found: Error: Can't resolve './development.keys' in '/tmp/build_43d652bc/packages/config/dist/keys'
What I did
Initially, I thought it was a problem with the environment variables and that in production the development file was wrongly required.
However, I later realized that this was not the problem. And indeed, even placing the import of the configuration file for development in any place of the code, even following a return, the error continues to occur.
import { merge } from "lodash";
import { IConfig, RecursivePartial } from "./interfaces";
import { defaultKeys } from "./default.keys";
import { existsSync } from "fs";
const secretKeys: RecursivePartial<IConfig> = (function (env) {
switch (env) {
case "production":
return require("./production.keys").productionKeys;
default:
try {
if (!existsSync("./development.keys")) {
return require("./production.keys").productionKeys; // <-------
} else {
return require("./production.keys").productionKeys; // <-------
}
require("./development.keys").developmentKeys; // <------- This line should not be executed
} catch (e) {
return require("./production.keys").productionKeys;
}
}
})(process.env.NODE_ENV);
export const keys = merge(defaultKeys, secretKeys) as IConfig;
It is as if during the build, nextjs (or probably webpack) controls all the imports, without following the "flow" of the code.
I hope someone shows me where I am wrong because at the moment I am stuck. Thank you!
Update
Thanks to the ideas of this discussion I changed my file which now looks like this:
const getConfigPath = (env?: string): string => {
console.log(env);
if (env === "production") return "production.keys";
else return "development.keys";
};
const secretKeys: RecursivePartial<IConfig> = require("./" +
getConfigPath(process.env.NODE_ENV)).keys;
export const keys = merge(defaultKeys, secretKeys) as IConfig;
However, now I'm running into another webpack error:
Module parse failed: Unexpected token (2:7)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
| import { IConfig, RecursivePartial } from "../interfaces";
> export declare const keys: RecursivePartial<IConfig>;
It is as if webpack does not recognize declaration files generated by typescript. However, the error is new and I have never encountered it. I believe it's due to the behavior of webpack pointed out in the linked discussion.
I trust in some help, since I know little about webpack
Edit
This is my next.config.js:
const path = require("path");
module.exports = {
distDir: '../../.next',
sassOptions: {
includePaths: [path.join(__dirname, 'styles')],
prependData: `#import "variables.module.scss";`
},
webpack: (config, { isServer }) => {
if (!isServer) {
config.resolve.fallback.fs = false;
}
return config;
},
};
Basically they are the defult configurations. The only thing I have changed is the path for the build, I have made a sass file globle and I have momentarily added a piece of code to allow the use of the fs module, which however as you can see above I do not use it more. So I could take this configuration out.
You need the loader for typescript configured in your next.config.js
npm install || yarn add awesome-typescript-loader
module.exports = {
distDir: '../../.next',
sassOptions: {
includePaths: [path.join(__dirname, 'styles')],
prependData: `#import "variables.module.scss";`
},
webpack: (config, options) => {
const { dir, defaultLoaders, isServer } = options;
if (!isServer) {
config.resolve.fallback.fs = false;
}
config.pageExtensions.push(".ts", ".tsx");
config.resolve.extensions.push(".ts", ".tsx");
config.module.rules.push({
test: /\.tsx?$/,
include: [dir],
exclude: /node_modules/,
use: [
defaultLoaders.babel,
{
loader: "awesome-typescript-loader",,
},
],
});
return config;
}
}

Definition for the custom rule was not found

I am not really strong with setting up my custom npm packages, therefore I am asking for help. I am trying to test my custom ESLint plugin rules on another project. For this, I have set up two projects in one root directory and linked them, using the file linking option (find the image of the file structure attached):
"eslint-plugin-winniepukki-guidelines": "file:../guide"
.eslintrc configuration file on the project, where I want to use my custom ESLint plugin:
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: [
'airbnb-base',
],
parserOptions: {
ecmaVersion: 12,
sourceType: 'module',
},
plugins: [
'winniepukki-guidelines',
],
rules: {
'winniepukki-guidelines/use-license': 'error',
'winniepukki-guidelines/avoid-names': 'error',
},
};
Unfortunately, the rules do not apply properly with the following error:
Definition for rule 'winniepukki-guidelines/avoid-names' was not found.eslint(winniepukki-guidelines/avoid-names). This is how the avoid-names.js file looks like:
module.exports = {
meta: {
messages: {
avoidName: "Avoid using variables named '{{ name }}'"
}
},
create(context) {
return {
Identifier(node) {
if (node.name === "foo") {
context.report({
node,
messageId: "avoidName",
data: {
name: "foo",
}
});
}
}
};
}
};
And this is the ESLint package's index.js file itself, illustrating how do I export them:
const path = require('path');
const requireIndex = require('requireindex');
module.exports.rules = requireIndex(path.join(__dirname, 'rules'));
Project overview: https://i.stack.imgur.com/hLKcH.png

how to test if babel works and my plugins are executed

I'm using Next.js for Server Side Rendering of React application (with styled-components) and have issue with Babel plugins I'm using to show name of the components I'm using in code.
This is my .babelrc file:
{
"env": {
"development": {
"presets": ["next/babel"],
"plugins": [
[
"babel-plugin-styled-components",
{
"ssr": true,
"displayName": true,
"preprocess": false
}
]
]
},
"production": {
"presets": "next/babel",
"plugins": [
[
"babel-plugin-styled-components",
{
"displayName": false,
"ssr": true
}
]
]
},
"test": {
"presets": [
[
"env",
{
"modules": "commonjs"
}
],
"next/babel"
]
}
}
}
When I'm running cross-env NODE_ENV=development concurrently "tsc --watch" next
I'm getting these lines - meaning .babelrc is used during copilation:
[1] > Using external babel configuration
[1] > Location: "...../.babelrc"
[1] > Using "webpack" config function defined in next.config.js.
But once I go to dev tools and see some styled-component I can see this: class="sc-iyvyFf gGaJAt" but on my code I have this definition:
const Title = styled.div`
font-size: 40px;
line-height: 1.13;
`
As it seems from documentation example - I should get something like ... <button class="Button-asdf123 asdf123" /> instead of just <button class="asdf123" />. But I don't.
After going deeper I found this issue ( https://github.com/styled-components/styled-components/issues/1103#issuecomment-324302997 ) based on errors I get in browser console that said:
It seems that only the server code is being transpiled and not the client code 😉
So Question: How to test if babel works correctly and .babelrc is being used in all places?
P.S. In my case those classes that I've got on client side had this prefix sc- meaning in my head styled-components. So I was not sure if the plugin from .babelrc works at all or it works, but I haven't set any special property in declaration of styled-components thus get this general prefix sc-
UPDATE Here is my custom next.conf.js I'm using
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
const { ANALYZE } = process.env
const path = require('path')
module.exports = {
exportPathMap: function() {
return {
'/': { page: '/' }
}
},
webpack: function(config) {
if (ANALYZE) {
config.plugins.push(
new BundleAnalyzerPlugin({
analyzerMode: 'server',
analyzerPort: 8888,
openAnalyzer: true
})
)
}
config.resolve.alias = {
'styled-components': path.resolve('./node_modules/styled-components/')
}
return config
}
}
Looks like no one has unfortunately answered this before;
What you're seeing probably comes down to this little piece of code you posted: tsc --watch
If you execute TypeScript transpilation before Babel and leave it up to TypeScript to transpile to ES5, it'll transpile all tagged template literals, not giving our plugin anything to hook onto.
Since you're already using next.js you won't need to set up your Babel pipeline from scratch. Rather you only need to disable this type of transpilation in TypeScript.
I'd suggest you to set target inside your tsconfig.json to ESNext so that it leaves everything up to Babel.
https://www.typescriptlang.org/docs/handbook/compiler-options.html (See "--target")

Categories

Resources