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",
}],
],
Related
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
I'm trying to concatenate two js files after the webpack build process. The goal is to provide a single js file with the ES6 modules and the legacy code in it.
Already tried plugins like webpack-concat-files-plugin without success. Which makes sense to me, because the output files are not there when the plugin gets executed.
Another thought would be a small script executed in the afterCompile hook, which handles the concatenation of the two files. Is this the common way to do something like this or are there other ways to achieve the goal?
Thanks for your help.
Basic example:
module.exports = {
entry: {
app: 'app.js',
legacy: [
'legacy-1.js',
'legacy-2.js',
// ...
]
},
output: {
filename: path.join('dist/js', '[name].js'),
}
}
Solved this as suggested:
FileMergeWebpackPlugin.js
const fs = require('fs');
class FileMergeWebpackPlugin {
constructor({ files, destination, removeSourceFiles }) {
this.files = files;
this.destination = destination;
this.removeSourceFiles = removeSourceFiles;
}
apply(compiler) {
compiler.hooks.afterEmit.tap('FileMergeWebpackPlugin', () => {
const fileBuffers = [];
this.files
.filter(file => fs.existsSync(file))
.forEach(file => fileBuffers.push(fs.readFileSync(file)))
fs.writeFileSync(this.destination, fileBuffers.concat(), { encoding: 'UTF-8' })
if (this.removeSourceFiles) {
this.files.forEach(file => fs.unlinkSync(file));
}
});
}
}
module.exports = FileMergeWebpackPlugin;
webpack.config.js
const FileMergeWebpackPlugin = require('./FileMergeWebpackPlugin');
module.exports = {
entry: {
app: 'app.js',
legacy: [
'legacy-1.js',
'legacy-2.js',
]
},
output: {
filename: path.join('dist/js', '[name].js'),
},
plugins: [
new FileMergeWebpackPlugin({
destination: 'dist/js/bundle.js',
removeSourceFiles: true,
files: [
'dist/js/app.js',
'dist/js/legacy.js',
]
})
]
}
Eventually i will release this as a npm package, will update the post when i do so
Yes.
You can create your own Plugin that is activate in emit hook - the parameter for this hook is compilation. You can get the chunks you look to concat from this object and create a new chunk with the concated value (or just add one to the other)
I'm using eleventy to create a static site with a sprinkle of JavaScript. I'm not using webpack or other bundlers. JavaScript is transpiled by calling 'transformFileAsync' via eleventys beforeBuild event. Here's the relevant part of eleventy config:
babel.transformFileAsync("src/assets/js/index.js").then((result) => {
fs.outputFile("dist/assets/main.js", result.code, (err) => {
if (err) throw err;
console.log("JS transpiled.");
});
});
My babel.config.js is as follows:
module.exports = (api) => {
api.cache(true);
const presets = [
[
"#babel/preset-env",
{
bugfixes: true,
modules: "systemjs",
useBuiltIns: "usage",
corejs: { version: 3, proposals: true },
},
],
];
const plugins = [];
return { presets, plugins };
};
Babel works as advertised and transpiles my js just fine. But I can't figure out how I can include (without help from a bundler) corejs polyfills in the final production bundle.
For example, the following code:
Array.from(document.getElementsByTagName("p")).forEach((p) => {
console.log(`p ${index}, startsWith('W')`, p, p.innerHTML.startsWith("W"));
});
Is transpiled to:
import "core-js/modules/es.array.for-each";
import "core-js/modules/es.array.from";
import "core-js/modules/es.string.iterator";
import "core-js/modules/es.string.starts-with";
import "core-js/modules/web.dom-collections.for-each";
System.register([], function (_export, _context) {
"use strict";
return {
setters: [],
execute: function () {
Array.from(document.getElementsByTagName("p")).forEach(function (p) {
console.log("p ".concat(index, ", startsWith('W')"), p, p.innerHTML.startsWith("W"));
});
}
};
});
How would I go about having the actual polyfill in the final bundle instead of all the imports?
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);
}
}
};
},
]
});```
I'm using Webpack's [hash] for cache busting locale files. But I also need to hard-code the locale file path to load it from browser. Since the file path is altered with [hash], I need to inject this value to get right path.
I don't know how can get Webpack [hash] value programmatically in config so I can inject it using WebpackDefinePlugin.
module.exports = (env) => {
return {
entry: 'app/main.js',
output: {
filename: '[name].[hash].js'
}
...
plugins: [
new webpack.DefinePlugin({
HASH: ***???***
})
]
}
}
In case you want to dump the hash to a file and load it in your server's code, you can define the following plugin in your webpack.config.js:
const fs = require('fs');
class MetaInfoPlugin {
constructor(options) {
this.options = { filename: 'meta.json', ...options };
}
apply(compiler) {
compiler.hooks.done.tap(this.constructor.name, stats => {
const metaInfo = {
// add any other information if necessary
hash: stats.hash
};
const json = JSON.stringify(metaInfo);
return new Promise((resolve, reject) => {
fs.writeFile(this.options.filename, json, 'utf8', error => {
if (error) {
reject(error);
return;
}
resolve();
});
});
});
}
}
module.exports = {
// ... your webpack config ...
plugins: [
// ... other plugins ...
new MetaInfoPlugin({ filename: 'dist/meta.json' }),
]
};
Example content of the output meta.json file:
{"hash":"64347f3b32969e10d80c"}
I've just created a dumpmeta-webpack-plugin package for this plugin. So you might use it instead:
const { DumpMetaPlugin } = require('dumpmeta-webpack-plugin');
module.exports = {
...
plugins: [
...
new DumpMetaPlugin({
filename: 'dist/meta.json',
prepare: stats => ({
// add any other information you need to dump
hash: stats.hash,
})
}),
]
}
Please refer to the Webpack documentation for all available properties of the Stats object.
Seems like it should be a basic feature but apparently it's not that simple to do.
You can accomplish what you want by using wrapper-webpack-plugin.
plugins: [
new WrapperPlugin({
header: '(function (BUILD_HASH) {',
footer: function (fileName) {
const rx = /^.+?\.([a-z0-9]+)\.js$/;
const hash = fileName.match(rx)[1];
return `})('${hash}');`;
},
})
]
A bit hacky but it works — if u don't mind the entire chunk being wrapped in an anonymous function.
Alternatively you can just add var BUILD_HASH = ... in the header option, though it could cause problem if it becomes a global.
I created this plugin a while back, I'll try to update it so it provides the chunk hash naturally.
On server, you can get the hash by reading the filenames (example: web.bundle.f4771c44ee57573fabde.js) from your bundle folder.
You can pass the version to your build using webpack.DefinePlugin
If you have a package.json with a version, you can extract it like this:
const version = require("./package.json").version;
For example (we stringified the version):
new webpack.DefinePlugin({
'process.env.VERSION': JSON.stringify(version)
}),
then in your javascript, the version will be available as:
process.env.VERSION
The WebpackManifestPlugin is officially recommended in the output management guide. It writes a JSON to the output directory mapping the input filenames to the output filenames. Then you can inject those mapped values into your server template.
It's similar to Dmitry's answer, except Dmitry's doesn't appear to support multiple chunks.
That can be done with Webpack Stats Plugin. It gives you nice and neat output file with all the data you want. And it's easy to incorporate it to the webpack config files where needed.
E.g. To get hash generated by Webpack and use it elsewhere.
Could be achieved like:
# installation
npm install --save-dev webpack-stats-plugin
yarn add --dev webpack-stats-plugin
# generating stats file
const { StatsWriterPlugin } = require("webpack-stats-plugin")
module.exports = {
plugins: [
// Everything else **first**.
// Write out stats file to build directory.
new StatsWriterPlugin({
stats: {
all: false,
hash: true,
},
filename: "stats.json" // Default and goes straight to your output folder
})
]
}
# usage
const stats = require("YOUR_PATH_TO/stats.json");
console.log("Webpack's hash is - ", stats.hash);
More usage examples in their repo
Hope that helps!