Webpack dynamic import .json file? - javascript

I'm using React Intl for x number of languages (example below) and at the moment Im importing the following where I setup my App:
import { addLocaleData } from 'react-intl';
import locale_en from 'react-intl/locale-data/en';
import locale_de from 'react-intl/locale-data/de';
import messages_en from './translations/en.json';
import messages_de from './translations/de.json';
addLocaleData([...locale_en, ...locale_de]);
...
export const messages = {
en: messages_en,
de: messages_de
}
Since these language files are being imported no matter which language is being used my main bundle js file is getting pretty big, especially from the .json files.
How can I with Webpack split these language files (or copy them to my dist folder using CopyWebpackPlugin) and then dynamically import them based on the language being used at the moment?
The app is isomorphic so this same code is being run on the server.

I've been working on something like this lately, although I don't need SSR for my project. I found that pairing dynamic import syntax with React's Suspense component achieves the desired result. Here's a rough overview of what I found to work, at least in my case, which doesn't include SSR:
// wrap this around your JSX in App.js:
<React.Suspense fallback={<SomeLoadingComponent />}>
<AsyncIntlProvider>
{/* app child components go here */}
</AsyncIntlProvider>
</React.Suspense>
// the rest is in support of this
// can be placed in another file
// simply import AsyncIntlProvider in App.js
const messagesCache = {};
const AsyncIntlProvider = ({ children }) => {
// replace with your app's locale getting logic
// if based on a hook like useState, should kick off re-render and load new message bundle when locale changes (but I haven't tested this yet)
const locale = getLocale();
const messages = getMessages(locale);
return (
<IntlProvider locale={locale} messages={messages}>
{children}
</IntlProvider>
);
};
function getMessages(locale) {
if (messagesCache[locale]) {
return messagesCache[locale];
}
// Suspense is based on ErrorBoundary
// throwing a promise will cause <SomeLoadingComponent /> to render until the promise resolves
throw loadMessages(locale);
}
async function loadMessages(locale) {
// dynamic import syntax tells webpack to split this module into its own chunk
const messages = await import('./path/to/${locale}.json`);
messagesCache[locale] = messages;
return messages;
}
Webpack should split each locale JSON file into its own chunk. If it doesn't, something is likely transpiling the dynamic import syntax to a different module system (require, etc) before it reaches webpack. For example: if using Typescript, tsconfig needs "module": "esnext" to preserve import() syntax. If using Babel, it may try to do module transpilation too.
The chunk output for a single locale will look something like this; definitely more than would be achieved via CopyWebpackPlugin:
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
/***/ "./path/to/en-US.json":
/*!*************************************!*\
!*** ./path/to/en-US.json ***!
\*************************************/
/*! exports provided: message.id, default */
/***/ (function(module) {
eval("module.exports = JSON.parse(\"{\\\"message.id\\\":\\\"Localized message text\\\"}\");//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiLi9zcmMvbG9jYWxpemF0aW9uL2VuLVVTLmpzb24uanMiLCJzb3VyY2VzIjpbXSwibWFwcGluZ3MiOiIiLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///./path/to/en-US.json\n");
/***/ })
}]);
Hopefully, this is a good starting point and either works with SSR or can be modified to work with SSR. Please report back with your findings on that subject. 🙂

Related

Installing Plaiceholder in Next.js / Webpack 5 causes: Module not found: Can't resolve 'child_process'

I'm building on Next.js app and when I install / import Plaiceholder (for generating placeholder images), I get the following error: Module not found: Can't resolve 'child_process'
Node v14.18.0
Next.js v11.1.2
Plaiceholder v2.2.0
Sharp v0.29.2
I understand this error message to mean that webpack5 is trying to bundle node packages that aren't available to the client. But I'm not clear why it is doing this. I haven't customized any of the webpack configs, and I can't find any mention of this issue in the Plaiceholder docs. How do I fix it?
NOTE: I want the base64 data URL to get created during the build, so that it available as soon as the page loads (not fetched asynchronously at run time).
Here's my next.config.js
module.exports = {
reactStrictMode: true,
};
My package.json only has scripts, dependencies, and devDependencies (nothing to change module resolution)
In case it's relevant, here's a simplified example using Plaiceholder:
import Image from "next/image";
import { getPlaiceholder } from "plaiceholder";
import React, { useState } from "react";
...
const { base64 } = await getPlaiceholder(imgUrl);
...
return (<Image
src={imgUrl}
placeholder="blur"
blurDataURL={base64}
/>);
It seems like plaiceholder is not suitable for client-side rendering. I believe that package is for the Node.js environment. That's why you get this error when you try to render your component on the client side.
To solve this problem, you need to move import { getPlaiceholder } from 'plaiceholder' to the NextJS API section. Then you can call that API with your URL data in the body. Then get the base64.
/api/getBase64.js
import { getPlaiceholder } from "plaiceholder";
export default async (req, res) => {
const { body } = req;
const { url } = body;
const { base64 } = getPlaiceholder(url);
res.status(200).send(base64);
};
/component.js
import Image from "next/image";
import React, { useState, useEffect } from "react";
const [base64, setBase64] = useState()
useEffect(() => {
(async () => {
const _base64 = await fetch.post('/api/getBase64', {url: imgUrl}); // wrote for demonstration
setBase64(_base64);
})()
})
return (<Image
src={imgUrl}
placeholder="blur"
blurDataURL={base64}
/>);
I know blurDataURL will be undefined until you fetch the data but this is the way how you can use plaiceholder library to manage your images. It should be imported only for the NodeJS environment. If you do not like this approach, you can try to find another library that also works for the browser environment (client)
UPDATED according to the comment:
If you want to generate this base64 at build time, you can use getStaticProps in the pages that use this Image component. NextJS is smart enough to understand which libraries are used in the client-side or server-side. So you can do this:
import { getPlaiceholder } from "plaiceholder"; // place it at the root of file. This will not be bundled inside of client-side code
export async function getStaticProps(context) {
const { base64 } = await getPlaiceholder(imgUrl);
return {
props: { base64 }, // will be passed to the page component as props
}
}
This way, by using getStaticProps, the page will be created at build time. You can get the base64 prop inside of the page that uses the image component and pass that prop to blurDataURL. Also, you can use this approach with getServerSideProps too.
This is from NextJS website:
Note: You can import modules in top-level scope for use in
getServerSideProps. Imports used in getServerSideProps will not be
bundled for the client-side.
https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering
It's necessary to Install plugin for Next Js dependency and configure next config based on Plaiceholder Docs for using getPlaiceholder() function in getStaticProps like the answer by #oakar.
npm i #plaiceholder/next
const { withPlaiceholder } = require("#plaiceholder/next");
module.exports = withPlaiceholder({
// your Next.js config
});

Importing / exporting Javascript Object Properties

I support a relatively complex legacy codebase, but am looking to modernise it a little by bringing in Webpack so that we'd have import & export capabilities in JS.
The problem I'm having is that we use a global object called App where we define and add different properties depending on the page. So for example we have the following file where we instantiate App (loaded on all pages):
app.js
const App = (() => {
const obj = {
Lib: {},
Util: {},
// etc
}
return obj;
})();
Then in another file we add to App.Lib just for the specific page that needs it:
lazyload.js
App.Lib.Lazyload = (() => {
// lazyload logic
})();
We simply concatenate the files during the bundling process, but obviously this is not ideal as none of the files have no knowledge of what goes on outside of it.
Exporting only seems to work for the top level object (where the object is defined), so anything I add to it elsewhere cannot be exported again. For example if I add export default App.Lib.Lazyload; at the end of lazyload.js and then try to import it elsewhere it will not import the Lazyload property.
Is there any way to get this to work without major refactor? If not, would you have any suggestions about the best way to handle it?
I don't think you can import Object.properties in JS. If you want to bundle specific packages (say Lazyload) for packages that need them, you might try:
//lazyload.js
export const LazyLoad = {
//lazyload logic
}
then somewhere else...
import {LazyLoad} from 'path/to/lazyload.js';
// assuming App has already been created/instantiated
App.Lib.Lazyload = LazyLoad;
Using Export Default...
//lazyload.js
const LazyLoad = {};
export default LazyLoad;
then...
import LazyLoad from 'path/to/lazyload.js';
App.Lib.LazyLoad = LazyLoad;
You can find help with Imports and Exports at MDN.

TypeScript type for module name string

Background:
TypeScript checks the modules that you try and import, and fails at compile-time if a string literal is used that does not match the name of an actual module or path to a supported file type.
For example import {Component} from 'react' and import item from './item' compile fine, but import {Component} from 'reactt' and import item from './otem' would fail to compile assuming that there is a react module installed and an item.ts file in the project but no reactt module installed and no ./otem file with the extensions .ts, .js, .json, etc in the project.
Problem: I'm not sure how to create a function where one of the arguments is a string literal that must correspond to an actual module or supported file.
function customImportEffect(moduleName: module_string_literal) {
...
}
What type should module_string_literal be so that compile-time checking fails when incorrect inputs such as 'reactt' or './otem' are supplied as arguments?
Reason for asking:
I am trying to make a webpack file transformer (a.k.a. a loader) which adds code to the top of TypeScript files before they are transpiled, so users can have type-safe hot module reloading code. For example, imagine the following code being inserted above every typescript file before being transpiled to JS:
class HotModule {
public static accept(moduleName: module_string_literal, onModuleReplace: Function(updatedModule: typeof import(moduleName)):void) {
if(module.hot) {
module.accept(moduleName.toString(), function() {
onUpdate(require(moduleName.toString()))
}
}
}
}
Then hot module reloading could be type-safe and appear more clean. Assuming that the default export from the ./item.ts file is an object with a member named message, one could write
HotModule.accept("./item", ({message: newMessage}) => {
const oldMessage = message;
messages[messages.indexOf(oldMessage)] = message = newMessage;
}
to implement hot reloading of the message instead of
if(module.hot) {
module.hot.accept("./item", function() {
const oldMessage = message;
messages[messages.indexOf(oldMessage)] = message = require("./item");
}
}
I am hoping this may be possible because since TypeScript v2.4 there has been the ability to asynchronously dynamically import modules, and the string literal supplied is checked for correctness, and incorrect string literals cause a compile-time failure.
Currently I'm just including "types": ["webpack-env"] in the compilerOptions object in my tsconfig.json file, but I'd like a hot module API which takes better advantage of TypeScript's checking toolset.
make a module-type.d.ts somewhere with the content
import json from "./package.json" // or whereever your package json is
export type ModuleName=keyof typeof json["dependencies"]
you will need to enable jsonmodule in your tsconfig
"resolveJsonModule": true,
then you import it wherever you need it

Jest vs React: module exports and "Jest encountered an unexpected token"

I am having problems getting React and Jest to work together which seems odd as I thought they both came from a similar origin. My issue is around the way that the class I'm testing is being exported.
I have an ArticleService class which I could use happily in React when it was exporting as default:
class ArticleService {
constructor(articles) {
this.articles = articles;
if (!this.articles) {
this.articles =
[//set up default data....
];
}
}
getAll(){
return this.articles;
}
}
//module.exports = ArticleService;//Need this for jest testing, but React won't load it
export default ArticleService;// Can't test with this but React will load it.
Here is how it is being called in my React app (from my HomeComponent):
import ArticleService from './services/xArticleService';
and is being used happily as
const articles = (new ArticleService()).getAll();
However my tests will not run. Here is my test with an import of the class file:
import ArticleService from "../services/xArticleService";
it('correctly gets all summaries', () => {
var summaries = getFakeSummaryList();
var testSubject = new ArticleService(summaries);
var actual = testSubject.getAll();
expect(actual.length).toEqual(10);
});
and I get
FAIL src/tests/ArticleService.test.js
Test suite failed to run
Jest encountered an unexpected token
This usually means that you are trying to import a file which Jest cannot parse, e.g. it's not plain JavaScript.
By default, if Jest sees a Babel config, it will use that to transform your files, ignoring "node_modules".
Here's what you can do:
• To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
• If you need a custom transformation specify a "transform" option in your config.
• If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.
You'll find more details and examples of these config options in the docs:
https://jestjs.io/docs/en/configuration.html
Details:
U:\...\src\tests\ArticleService.test.js:2
import ArticleService from "../services/xArticleService";
^^^^^^
SyntaxError: Unexpected token import
at ScriptTransformer._transformAndBuildScript (node_modules/jest-runtime/build/script_transformer.js:403:17)
If (in the test) I swop
import ArticleService from "../services/xArticleService";
for
const ArticleService = require('../services/xArticleService');
and edit the export in xArticleService.js to
module.exports = ArticleService;//Need this for jest testing, but React won't load it
then the tests are executed but React will not load it:
Attempted import error: './services/xArticleService' does not contain a default export (imported as 'ArticleService').
I have a default set up, creating using create-react-app. I've not changed any .babelrc.
Can anyone see where I am going wrong?
Thanks
UPDATE:
I have made the changes to .babelrc suggested in the accepted answer to the possible duplicate of this but this has made no change to the output.

Testing React Components: How does it work?

In the jest docs, I found this simple example of testing react components:
// Link.react.test.js
import React from 'react';
import Link from '../Link.react';
import renderer from 'react-test-renderer';
test('Link changes the class when hovered', () => {
const component = renderer.create(
<Link page="http://www.facebook.com">Facebook</Link>,
);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
// manually trigger the callback
tree.props.onMouseEnter();
// re-rendering
tree = component.toJSON();
expect(tree).toMatchSnapshot();
// manually trigger the callback
tree.props.onMouseLeave();
// re-rendering
tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
Why do we have to import React and react-test-renderer, but not have to import other test specific things, like test, expect?
Can someone explain, how this works under the hood and what actually happens when the tests are run?
It finds the binary jest and executes it with your script, this binary would compile your code first then run it, so those modules for testing would be imported during compiling time when those function keyword was found. You install Jest to your original application to test component. React module or others it really your stuff.
Update
By tracing the repository of Jest
jest/packages/jest-runtime/src/script_transformer.js, We could found out it utilize Node.js module VM to run the script, and it has some method like vm.createContext() and vm.Script().runInContext(), so those internal module should be imported to sandbox programmatically.
Example from VM
const vm = require('vm');
const sandbox = { globalVar: 1 }; // <=> import expect, test
vm.createContext(sandbox);
...
vm.runInContext('globalVar *= 2;', sandbox); // <=> Our test code.
So those module such as expect, and test may be imported like what vm.createContext() does above.
It's hard to exactly know how this be done in a short time, but we still could get some clues:
in jest/packages/jest-runtime/src/cli/index.js
...
import Runtime from '../'; // ---> jest/packages/jest-runtime/src/index.js
export function run(...) {
...
Runtime.createContext(
...
).then(
const runtime = new Runtime(config, environment, hasteMap.resolver);
runtime.requireModule(filePath);
...
)
}
Runtime is a critical class defined in
jest/packages/jest-runtime/src/index.js
...
import Resolver from 'jest-resolve';
...
import ScriptTransformer from './script_transformer';
...
requireModule() {
_execModule(...)
}
...
_execModule() {
...
this._createRequireImplementation(
...
this._createJestObjectFor(...)
}
Many critical works here, require module, detect the environment config, has Resolver to find the module id, to detect what kind of the module, should it be mocked, return jestObject, wrap all to our sandbox for testing.
Here is its core to do mock

Categories

Resources