I have a React application, and I'm building the React components inside a seperate library so that it can be shared throughout other applications. As the React Components are private, they're installed in NPM via git+ssh, rahter than as an NPM package.
The basics:
My component Library has this structure:
-- components
---- component1.jsx
---- component2.jsx
---- index.js
-- package.json
The library's package.json file looks basically like this:
{
"name": "my-react-components",
"version": "1.0.0",
"description": "Collection of React components",
"main": "components/index.js",
"dependencies": {
...
}
}
The components/index.js file looks like this
export * from './component1';
export * from './component2';
Each component is written like this:
export const SomeComponent = () => { ... };
My application references these components like this
import { SomeComponent } from 'my-react-components';
My NPM dependency looks like this
"my-react-components": "git+ssh://{gitaddress}"
Here's my applications webpack config
Since I'm developing this library in unison with my application, I have been using a symlink from the library folder to the application's node_modules folder to allow the references. This has been working, and is continuuing to work fine.
The problem
When I don't use a symlink and instead use the version installed by NPM, babel or webpack or some part of the puzzle suddenly stops building my component library, and instead of referencing the components, I end up with the export * from ... tags in my compiled bundle. Cleaning the package and returning to the symlink method, and everything works fine again.
Webpack shows no errors, everything appears to build fine. I can see the difference in output however, in the symlink method, I can see each file being written into the bundle:
[1] ./node_modules/my-react-components/components/component1.jsx
[2] ./node_modules/my-react-components/components/component1.jsx
// Something like that I just made that up,
// the point is I see the paths for components that are being referenced.
When I don't use the symlink I instead just see something like
[1] ./node_modules/my-react-components/components/index.js
and as previously mentioned, that file is basically dropped straight in, no babel, or checking imports or anything.
So why does this happen with an NPM installed module, but not the exact same files in the exact same location through a symlink?
Just to note they are definitely the exact same files, I've done this immediately after pushing my lib and making no changes, deleting the node_modules folder and reinstalling, re-symlinking etc. The difference in the two methods is reproducable every time.
Related
We have a yarn 2 monorepo setup with the following workspaces:
/root
/app (nextjs)
/components (individual react functional components)
/storybook
/constants
Currently, /app has the i18next translation files stored local to the workspace, but I would like to move that into the constants workspace so that all workspaces can share the same translations. I had no issue moving the translations there and loading them in both the /app and /storybook workspaces.
Also, currently, all translations happen only in /app. The /components workspace has no translations, and the translated text is passed in props to the dumb components. So, a component in /app looks something like this:
import ListBox from "#root/components/ListBox";
import {useTranslation} from "react-i18next";
export default const Page() {
const [t] = useTranslation();
const label = t("listBoxLabel"); // <-- exists in en.json as a key
return (
<div>
<ListBox label={label} />
</div>
)
}
As such, storybook also has to provide the "label" prop to render the component in stories, doing this in pretty much the same manner.
What I would like to do is instead translate the text at the /components workspace level, so that the translations can be done in one place, where it's used, to reduce prop drilling and also simplify things.
Something like this:
/components/ListBox.js:
import {useTranslation} from "react-i18next";
export default const ListBox() {
const [t] = useTranslation();
const label = t("listBoxLabel"); // <-- exists in en.json as a key
return <p>{label}</p>
}
When I run this code, it just prints out the translation key "listBoxLabel", in both storybook and the nextjs app.
I was able to fix storybook by wrapping stories with I18nextProvider and changing the dependencies in the /components workspace for i18next and react-i18next to move them from a normal dependency to a peer dependency, and storybook properly renders the translation. However, then the NextJS /app throws an error that the react-i18next Module Not Found when it imports the /components/ListBox.js code. It seems to require the /components workspace keep a normal dependency for that, but doesn't translate it in that case. If I remove the /components dependencies altogether, /storybook won't compile and start for the same reason.
Duplicating this exact code in the app or storybook, however, displays the correct translation. So, it has something to do with importing it from a sibling workspace in the monorepo, and I'm not sure exactly how to go about fixing that for the nextjs app.
Just a note: I don't use the next-i18next component. Instead, I am simply using react-i18next directly, without any providers configured. I did attempt to wrap the app with the I18nextProvider component similar to the storybook stories, but it didn't work.
Does anyone have any ideas where to look further or what to try for this? I would think loading a component from a workspace component library where the translations happen within the library would be a common scenario, but I haven't been able to find anyone else running into this issue.
I don't know exactly what's causing this, but it appears to be a nextjs, webpack, or yarn bug. I discovered the problem was related to this in my tsconfig.js file in the #root/app workspace:
{
"compilerOptions": {
"paths": {
"#root/components/*": ["../components/*"]
}
}
}
I was able to fix this by removing this setting, but it broke other code (some components were typescript), so I just explicitly declared those component modules in the custom.d.ts file as modules.
Where the next/yarn bug comes in: after I removed this line, and restarted the dev server, the i18n translations were still broken. Until I went and touched the components/ListBox.js file. This caused something to rebuild within next, and the translations showed up.
The ListBox.js file is not typescript, so I don't understand why this causes a problem.
While trying to resolve this issue, yarn kept getting into a bad state, next was telling me I didn't have typescript installed when I did, and multiple other tedious random errors...what a mess.
With the new outputStandalone experimental feature (https://nextjs.org/docs/advanced-features/output-file-tracing#automatically-copying-traced-files-experimental) we can, after the build, have a standalone folder that contains the necessary dependencies, we simply copy it into our docker and don't need to rebuild inside the docker. It automatically detects the necessary dependencies in the source code UNLESS that dependency is only used in our package.json.
We use cross-env to start our Next app and of course this library is never imported in our source code, yet it needs to be present in the node_modules of the standalone folder.
So how can I force #vercel/nft to include a specific dependency ?
My case was that our dockerized Next.js project used Knex.js, where migrations were invoked with a npm script. Following this, some dependencies only required for knex.migrations() were never included in the standalone output.
Credits to sbstn, you can use unstable_includeFiles to create a list of dependencies, which should be included in the standalone output. This solution scans for .js and .json in a node_modules/ directory.
It should be highlighted that this setting is not available inside next.config.js, but as a page config prop, meaning it has to be exported inside a export const config = {} from a page different from _app.jsx and /api routes.
// pages/index.jsx
export default function Home(){
...
}
const requiredStandaloneDependencies = [
// some required deps that have not been included in standalone
"commander",
"debug",
"escalade",
"get-package-type",
"getopts",
"interpret",
"lodash",
"pg-connection-string",
"rechoir",
"resolve-from",
"tarn",
"tildify",
];
export const config = {
unstable_includeFiles: requiredStandaloneDependencies.map(
(dep) => `node_modules/${dep}/**/*.+(js|json)`
),
};
As a final thought for my case, where migrations are invoked with a custom npm script, npm run migrate:latest, I should also be able to circumvent the issue using programmatically invoked migrations. I'm just unsure as to how I add the method to the Next.js project tree to be run during runtime initialization.
I'm trying to import in Typescript some SVG icons, but I'm facing some problems.
At the first time I tried to import them, Typescript wasn't able to recognize the file extension.
I solved this issue by creating, as suggested in other Stack Overflow and Github topics, a custom.d.ts file with this rule inside:
declare module "*.svg" {
const content: React.StatelessComponent<React.SVGAttributes<SVGElement>>;
export default content;
}
But the problems seem to not finish here, even if the compilation seems going fine.
The current project I'm working on, is structured this way:
Typescript + React package (with SVG icons files) (SDK)
React Internal Sample page (package) to use the SDK
other internal packages...
For our development phase, we build through Webpack all the packages through different loaders and see the result through the Sample page.
But the final product flow to production is quite different: I export the SDK as CommonJS to an internal NPM Registry so another company can use it in a React project (the equivalent of the Sample page but for production) and push to production the final Webpack bundles with both projects inside.
So, to load in the Sample application the SVG icons, I'm using #svgr/webpack loader, which converts the files.
But when I have to export the SDK through npx tsc, I see that the exported folder, does not contain the folders with svg files.
I've tried to include them in tsconfig.json/files, but got this error:
TS6054: File '<path>/*.svg' has an unsupported extension. The only supported extensions are '.ts', '.tsx', '.d.ts'.
So, to attempt exporting them I converted my exporting script to use #svgr/cli to export the files to React files from SVGs before compiling to typescript:
// package.json
scripts: {
"build-ts": "rm -rf ./lib; yarn convert-svg-to-react; npx tsc",
"convert-svg-to-react": "npx #svgr/cli -d src src --typescript",
}
In this way, I get the new Typescript files mixed with the SVGs inside the package (so I'll have to remove them later) and I can see them in the exported folder lib.
But watching inside the Typescript exported code, I can see this line (for each svg import):
var close_svg_1 = __importDefault(require("./icons/close.svg"));
Leaving out the Typescript function for Babel __importDefault, you can see that it still requires the file svg, but what I have at this point, are the React components that replaces those files.
During development it works fine because #svgr/webpack loader, resolves the svg files.
But requiring svg files that do not exist, should make the application above it crash.
So, I'm stuck and I need some clues to get out of this situation.
Some clues that I got (but wasn't able to find how to do that), were:
[Best] Find how I can export raw svg files as they are during Typescript compilation without doing that manually, as they are not all in one folder but divided per components areas in the package tree. Doing this, I would tell the other company to add #svgr/webpack to its own building process.
Find how can I tell Typescript to import svg files without specify the extension (currently, removing .svg probably makes it fallback to .ts/tsx and therefore it cannot find the file with that name). In this way, the require would keep requiring the same file name but I could convert SVG to React Components without occurring in problems. But this would also require Typescript to export the file
Otherwise, I should convert all the SVGs in React components and directly use them instead of making them being compiled by #svgr/webpack, but I'm not sure this would have some other side-effects.
Any other clues or any way to achieve the ideas I got? Thank you everybody.
I'm brand new to React and trying to figure out how to use Ract-Recaptcha library (https://github.com/appleboy/react-recaptcha) without NPM.
here is the source code for the Recaptcha wrapper : index.js
Looks like the library imports import PropTypes from 'prop-types';
and in my env NPM is disabled, so I'm wrecking my head ( with very limited React knowledge) trying to understand how to do without PropTypes.
Very much appreciate any help !!
Clone the package into a lib directory or private_modules and import it manually and let Webpack (or whatever module bundler you use) resolve the dependency like code you've written.
In your package.json you can do something like:
{
"name": "MyApp",
"dependencies": {
"myLocalModule": "file:./lib/myLocalModule/dist/index.js"
}
}
Remember, this is our virtual world. There is always a way or at worst a hack! Cheers
https://docs.npmjs.com/files/package.json#local-paths
I have spent a lot of time looking into this, but to no avail. I am aware of how code splitting and dynamic bundling works in Webpack using the import promise API.
Howevr, my use case is that I have two completely separate bundles, generated separately using different webpack builds. To give you perspective, I am building React components and there is a requirement to dynamically load a react component into the page that has been compiled in a different process. Is this possible in react? I do have control over both webpack builds, so I can exclude dependencies, etc.
Update: I just looked at Vue.js, and how it allows developers to register Vue.js components and then reference them later in the code. I could potentially load my Vue.js component scripts before my page script. I'm trying to see if I can do something similar in React.
Did I understand you correctly: you have essentially got
a library of custom React components (built by Webpack build #1)
a React app that needs to use some (all) of these components (built by Webpack build #2, totally separate from #1)
?
If yes, then read on.
The "Is this possible in react?" question should instead be "Is this possible in Webpack?", and the answer is "Yes". The following is tested with Webpack 2, but should also work with v.1.
Let's call your projects Lib (your React component library) and App (the library consumer).
In the Lib project:
Create an entry point file, say index.js, that exports all the custom React components like this:
import {Button} from './button';
import {DatePicker} from './DatePicker';
import {TextBox} from './textBox';
export const MyComponentLib = {
Button,
DatePicker,
TextBox
};
Update webpack.config.js to make the project's bundle a UMD library (could also be 'var'), and set the entry point to the above index.js file. Doing so will make your library available via a global variable named MyComponentLib (the name comes from the export above) in the consuming app later on:
...
output: {
path: './dist',
filename: 'mylib.bundle.js',
libraryTarget: 'umd'
},
...
entry: './index.js',
...
On to the App project:
In the index.html file you will have two <script> tags: one for mylib.bundle.js (the output of the Lib project), and another for the bundle of the App project itself. You might have more bundles (app, vendor etc.), I'm just simplifying things here.
Update webpack.config.js to mark the component library as external dependency. Here, MyComponentLib is, again, the name of the global variable the library is available at, and myComponents is the name to use in import statements:
...
externals: {
myComponents: 'MyComponentLib'
},
...
Now, in App you can import a component like this:
import {DatePicker} from 'myComponents';
This will dynamically load DatePicker from the component library at run time via the global variable.
Bonus: if you use eslint, you don't want it to complain about missing modules that you know are external; add this to your .eslintrc:
...
"settings": {
"import/core-modules": ["myComponents"]
},
...