I have a lerna monorepo containing lots of packages.
I'm trying to achieve the following:
Ensure that VSCode provides the correct import suggestions (based on package names, not on relative paths) from one package to another.
Ensure that I can 'Open Definition' of one of these imports and be taken to the src of that file.
For 1. I mean that if I am navigating code within package-a and I start to type a function exported by package-b, I get a suggestion that will trigger the adding of an import: `import { example } from 'package-b'.
For 2. I mean that if I alt/click on the name of a function exported by 'package-b' while navigating the file from a different package that has imported it, I am taken to '/packages/namespace/package/b/src/file-that-contains-function.js',
My (lerna) monorepo is structured as standard, for example here is a 'components' package that is published as #namespace/components.
- packages
- components
- package.json
- node_modules
- src
- index.js
- components
- Button
- index.js
- Button.js
- es
- index.js
- components
- Button
- index.js
- Button.js
Note that each component is represented by a directory so that it can contain other components if necessary. In this example, packages/components/index exports Button as a named export. Files are transpiled to the package's /es/ directory.
By default, VSCode provides autosuggestions for imports, but it is confused by this structure and, for if a different package in the monorepo needs to use Button for example, will autosuggest all of the following import paths:
packages/components/src/index.js
packages/components/src/Button/index.js
packages/components/src/Button/Button.js
packages/components/es/index.js
packages/components/es/Button/index.js
packages/components/es/Button/Button.js
However none of these are the appropriate, because they will be rendered as relative paths from the importing file to the imported file. In this case, the following import is the correct import:
import { Button } from '#namespace/components'
Adding excludes to the project's jsconfig.json has no effect on the suggested paths, and doesn't even remove the suggestions at /es/*:
{
"compilerOptions": {
"target": "es6",
},
"exclude": [
"**/dist/*",
"**/coverage/*",
"**/lib/*",
"**/public/*",
"**/es/*"
]
}
Explicitly adding paths using the "compilerOptions" also fails to set up the correct relationship between the files:
{
"compilerOptions": {
"target": "es6",
"baseUrl": ".",
"paths": {
"#namespace/components/*": [
"./packages/namespace-components/src/*.js"
]
}
},
}
At present Cmd/Clicking on an import from a different package fails to open anything (no definition is found).
How should I configure VSCode so that:
VSCode autosuggests imports from other packages in the monorepo using the namespaced package as the import value.
Using 'Open Definition' takes me to the src of that file.
As requested, I have a single babel config in the root:
const { extendBabelConfig } = require(`./packages/example/src`)
const config = extendBabelConfig({
// Allow local .babelrc.js files to be loaded first as overrides
babelrcRoots: [`packages/*`],
})
module.exports = config
Which extends:
const presets = [
[
`#babel/preset-env`,
{
loose: true,
modules: false,
useBuiltIns: `entry`,
shippedProposals: true,
targets: {
browsers: [`>0.25%`, `not dead`],
},
},
],
[
`#babel/preset-react`,
{
useBuiltIns: true,
modules: false,
pragma: `React.createElement`,
},
],
]
const plugins = [
`#babel/plugin-transform-object-assign`,
[
`babel-plugin-styled-components`,
{
displayName: true,
},
],
[
`#babel/plugin-proposal-class-properties`,
{
loose: true,
},
],
`#babel/plugin-syntax-dynamic-import`,
[
`#babel/plugin-transform-runtime`,
{
helpers: true,
regenerator: true,
},
],
]
// By default we build without transpiling modules so that Webpack can perform
// tree shaking. However Jest cannot handle ES6 imports becuase it runs on
// babel, so we need to transpile imports when running with jest.
if (process.env.UNDER_TEST === `1`) {
// eslint-disable-next-line no-console
console.log(`Running under test, so transpiling imports`)
plugins.push(`#babel/plugin-transform-modules-commonjs`)
}
const config = {
presets,
plugins,
}
module.exports = config
In your case, I would make use of lerna in combination with yarn workspaces.
When running yarn install, all your packages are linked under your #namespace in a global node_modules folder. With that, you get IntelliSense.
I've set up an example repository here: https://github.com/flolude/stackoverflow-lerna-monorepo-vscode-intellisense
You just need to add "useWorkspaces": "true" to your lerna.json
lerna.json
{
"packages": ["packages/*"],
"version": "0.0.0",
"useWorkspaces": "true"
}
And the rest is just propper naming:
global package.json
{
"name": "namespace",
// ...
}
package.json of your component package
{
"name": "#namespace/components",
"main": "src/index.js",
// ...
}
package.json of the package that imports the components
{
"name": "#namespace/components",
"main": "src/index.js",
"dependencies": {
"#namespace/components":"0.0.0"
}
// ...
}
Then you can do the following:
import { Component1 } from '#namespace/components';
// your logic
Automatically Importing from #namespace
Unfortunately, I couldn't find a way to make this work in VSCode with a Javascript Monorepo. But there are some things you can do:
Use Typescript (tutorial, other tutorial)
Use module-alias
Add import {} from '#namespace/components' to the top of your file
Use Auto Import Extension
Edit: This is broken with the latest version of VSCode.
I finally managed to get this working reliably. You need to create a separate jsconfig.js for every package in your monorepo, for example:
{monorepo root}/packages/some-package/jsconfig.json:
{
"compilerOptions": {
"target": "es6",
"jsx": "preserve",
"module": "commonjs"
},
"include": ["src/**/*.js"],
"exclude": ["src/index.js"]
}
Note that I've excluded the src/index.js file so it doesn't get offered as an import suggestion from within that package.
This setup appears to achieve:
Intellisense import suggestions from packages instead of using relative paths.
Go to definition to source of other packages in the monorepo.
VSCode has been pretty flaky of late, but it seems to be working.
Note this is working for a JavaScript-only monorepo (not Typescript).
Related
So I've got a monorepo I am trying to use based on this one:
https://github.com/GeekyAnts/nativebase-templates/tree/master/solito-universal-app-template-nativebase-typescript
This repo has 4 separate places where there is a package.json (and theoretically a tsconfig.json) - the app is partially TypeScript and partially Javascript (re-writing it gradually in TypeScript as time goes along)
Basically the directory structure looks like this:
/ - root directory
/apps/expo - expo-related packages, configurations, functions, etc
/apps/next - next-related packages, configuration, functions, etc
/packages/app/ - general business logic, code, pages, etc
No matter what I am trying to do or where I am trying to setup the routing, it isn't working.
Inside my tsconfig.json in my root folder, I have this (I have also tried putting it in the individual tsconfig.json files of the individual folders):
"paths": {
"app/*": ["./packages/app/*"],
"components/*": ["./packages/app/components"],
"config/*": ["./packages/app/config"],
"controllers/*": ["./packages/app/controllers"],
"pages/*": ["./packages/app/pages"],
"reducers/*": ["./packages/app/redux"],
"resources/*": ["./packages/app/resources"],
"revenuecat/*": ["./packages/app/revenuecat"],
"routing/*": ["./packages/app/routing"],
"utils/*": ["./packages/app/utils"],
"interfaces/*": ["./packages/app/interfaces"],
"root/*": ["./*"]
}
But none of these paths are recognized in my Next app.
I've tried putting in the babel.config.js of the Expo folder inside my plugins:
[
'module-resolver',
{
root: '../../packages',
alias: {
app: '../../packages/app',
components: '../../packages/app/components',
config: '../../packages/app/config',
controllers: '../../packages/app/controllers',
pages: '../../packages/app/pages',
reducers: '../../packages/app/redux',
resources: '../../packages/app/resources',
revenuecat: '../../packages/app/revenuecat',
routing: '../../packages/app/routing',
utils: '../../packages/app/utils',
interfaces: '../../packages/app/interfaces',
},
},
]
I've tried putting them in the .babelrc of the Next folder, also in my plugins:
[
"module-resolver",
{
"root": "../../packages",
"alias": {
"app/*": "../../packages/app",
"components": "../../packages/app/components",
"config": "../../packages/app/config",
"controllers": "../../packages/app/controllers",
"pages": "../../packages/app/pages",
"reducers": "../../packages/app/redux",
"resources": "../../packages/app/resources",
"revenuecat": "../../packages/app/revenuecat",
"routing": "../../packages/app/routing",
"utils": "../../packages/app/utils",
"interfaces": "../../packages/app/interfaces"
}
}
]
The code I am trying to run is my _app.js which calls my Footer file in my /packages/app/components/main folder. The _app.js works fine and it gets to my Footer.web.js file, but then I get:
error - ../../packages/app/components/main/Footer.web.js:4:0
Module not found: Can't resolve 'components/main/AppButtons'
2 | import { FontAwesome } from '#expo/vector-icons';
3 | import moment from 'moment';
> 4 | import AppButtonGroup from 'components/main/AppButtons';
5 | import {
6 | Row,
7 | Column,
Now Appbuttons.tsx is in the same folder as Footer.web.js
My guess is that I need another .babelrc file for my /packages/app folder? Or is it another error?
My workspaces are set like this in my root package.json:
"workspaces": [
"apps/*",
"packages/*"
],
What is causing this to not work? Why is my pathing not working in my /packages/app folder?
Solution
Add your path alias into the workspace root tsconfig.json, also add "baseURL": "./".
{
"compilerOptions": {
+ "baseUrl": "./",
"strictNullChecks": true,
"noUncheckedIndexedAccess": true,
"paths": {
"app/*": ["./packages/app/*"],
+ "components/*": ["./packages/app/components/*"],
+ "config/*": ["./packages/app/config"],
// ...
Config for next, install tsconfig-paths-webpack-plugin, go to apps/next/next.config.js and apply this plugin:
const path = require('node:path');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
// ...
module.exports = withNativebase({
// ...
nextConfig: {
webpack: (config, options) => {
// ...
config.resolve.plugins = [
...config.resolve.plugins,
new TsconfigPathsPlugin({
// Use the root `tsconfig.json`!
configFile: path.resolve(__dirname, '../../tsconfig.json'),
}),
]
return config
},
Config for expo, go to apps/expo/babel.config.js:
const path = require('node:path');
// ...
plugins: [
'react-native-reanimated/plugin',
[
'module-resolver',
{
root: path.resolve(__dirname, '../../'),
alias: {
app: './packages/app',
components: './packages/app/components',
// ...
},
},
]
]
Explanation
First of all, let's me spit it out, I personally would've just use "app/components/*" as alias, so to avoid this config madness. But since you asked, here's the answer. See above for the solution, and below is some nerdy explanation on technical details. Skip if you don't care.
Truth of the app/* pseudo-alias
Without above config, only app/* alias works. But the reason's kinda special: this is NOT a real (in bundle tool's term) alias. It's in fact a real filesystem symlink <root>/node_modules/app -> <root>/packages/app.
This symlink is created by yarn/npm's workspace mechanism. Because it lives in <root>/node_modules, the alias-ish app/* path is actually resolved as if it's a real npm package.
One does not simply use alias
Now if you want to add your own real alias, you'll have to wrestle with build tools.
The template you use has two tooling setup: next for web, expo for native. You need to address them separately.
In this case, tsconfig.json -> compilerOptions.paths is mainly for IDE hint (e.g. vscode jump to definition). By default it's not used by either tools. So you still need to manually config alias via plugins like I've shown in solution section.
So yeah, 3 configs for 3 tools, madness.
Revisit pseudo-alias
Another solution is to leverage the workspace mechanism to create pseudo-alias, like in the case of app/*. For example, if you want components/* as "alias". You need to:
create <root>/packages/components/package.json and give it a "name": "components".
A lil twist: if you use "name": "duck" for whatever reason, your pseudo-alias will become duck/*, i.e., folder name does NOT matter. Only package name matters.
cd back to project root dir, and simply run yarn, you should now see a symlink at <root>/node_modules/components.
(optional step) You are supposed to add into "dependencies": { "components": "*" } in other workspace package's package.json (e.g., apps/next/package.json) if you want to use this components/* "alias", because it's in fact is another package named "components" that you depend on. It's a good practice to add dep package into "dependencies" field.
Quite laborious too, I know. But still an option.
This does not answer your question, because you do not use webpack, but you can try this tool. The code will look like below.
Try to start your paths from "#".
Try to move your config into the root folder.
const {resolve} = require("path");
const TsConfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
// ...
module.exports = {
entry: "./packages/app/index.tsx",
target: "web",
output: {
path: resolve(__dirname, "build"),
filename: "[name].js",
publicPath: "/",
chunkFilename: "[name].chunk.js"
},
// ...
resolve: {
extensions: [".js", ".jsx", ".json", ".ts", ".tsx"],
plugins: [new TsConfigPathsPlugin()],
alias: {
"#": resolve(__dirname, "packages/app"),
"#components": resolve(__dirname, "packages/app/components"),
"#config": resolve(__dirname, "packages/app/config"),
"#pages": resolve(__dirname, "packages/app/pages"),
// ...
}
},
// ...
};
tsconfig will look like this:
{
"compilerOptions": {
"jsx": "react",
"module": "commonjs",
"noImplicitAny": true,
"outDir": "./build/",
"preserveConstEnums": true,
"removeComments": true,
"sourceMap": true,
"target": "es2015",
"esModuleInterop": true,
"baseUrl": "./packages/app",
"strict": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"paths": {
"#components/*": ["./components/*"],
"#components": ["./components"],
"#config/*": ["./config/*"],
"#config": ["./config"],
"#pages/*": ["./pages/*"],
"#pages": ["./pages"],
// ...
"#/*": ["./*"]
}
},
"include": [
"packages/app"
]
}
I would do something basic which might not work for your use case,
I'd use npm install <folder>
Since these dirs are npm packages,
/ - root directory
/apps/expo - expo-related packages, configurations, functions, etc
/apps/next - next-related packages, configuration, functions, etc
/packages/app/ - general business logic, code, pages, etc
If I wanted to access /packages/app/ in apps/next dir, I'd do,
cd apps/next
npm install ../../packages/app --install-links
Assuming the name of package packages/app (in package.json) is my-app-packages I'd just use those like so,
import Routing from "my-app-packages/routing";
I believe you accidentally added /* to the end of all path like app/*. Try changing your paths in tsconfig.json like below
"paths": {
"app/*": ["./packages/app/*"],
"components": ["./packages/app/components"],
"config": ["./packages/app/config"],
"controllers": ["./packages/app/controllers"],
"pages": ["./packages/app/pages"],
"reducers": ["./packages/app/redux"],
"resources": ["./packages/app/resources"],
"revenuecat": ["./packages/app/revenuecat"],
"routing": ["./packages/app/routing"],
"utils": ["./packages/app/utils"],
"interfaces": ["./packages/app/interfaces"],
"root": ["./*"]
}
TL;DR
How can you tell SWC to compile CSS files imported in React components?
How can you tell SWC to compile absolute imports in tests and in React components?
Here is a minimal reproducible example.
Context
We're migrating from Babel to SWC. (I asked a question a little while ago. I'm improving on that question's answer.)
We're migrated the command from:
"test": "NODE_ENV=test riteway -r #babel/register 'src/**/*.test.js' | tap-nirvana",
to
"test": "SWC_NODE_PROJECT=./jsconfig.json riteway -r #swc-node/register src/**/*.test.js | tap-nirvana",
where the jsconfig.json looks like this:
{
"compilerOptions": {
"allowJs": true,
"baseUrl": "./src",
"jsx": "react-jsx"
}
}
If we write try to compile a test for a self-contained component (no absolute imports, no CSS) it works:
import { describe } from 'riteway';
import render from 'riteway/render-component';
function HomePageComponent({ user: { email } }) {
return <p>{email}</p>;
}
describe('home page component', async assert => {
const user = { email: 'foo' };
const $ = render(<HomePageComponent user={user} />);
assert({
given: 'a user',
should: 'render its email',
actual: $('p').text(),
expected: user.email,
});
});
The test compiles fine.
With Babel we had a .babelrc like this:
{
"env": {
"test": {
"plugins": [
[
"module-resolver",
{
"root": [
"."
],
"alias": {
"components": "./src/components",
"config": "./src/config",
"features": "./src/features",
"hocs": "./src/hocs",
"hooks": "./src/hooks",
"pages": "./src/pages",
"redux": "./src/redux",
"styles": "./src/styles",
"tests": "./src/tests",
"utils": "./src/utils"
}
}
]
]
}
},
"presets": [
[
"next/babel",
{
"ramda": {}
}
]
],
"plugins": [
["styled-components", { "ssr": true }]
]
}
Where the styles where taken care of by styled-components and the absolute imports where defined via the module-resolver plugin. (We switched away from styled-components to CSS modules, which is why we import from .module.css CSS files. Anyways ...)
If we write the test how we wanted to write it with their actual imports like this:
import { describe } from 'riteway';
import render from 'riteway/render-component';
import { createPopulatedUserProfile } from 'user-profile/user-profile-factories';
import HomePageComponent from './home-page-component';
describe('home page component', async assert => {
const user = createPopulatedUserProfile();
const $ = render(<HomePageComponent user={user} />);
assert({
given: 'a user',
should: 'render its email',
actual: $('p').text(),
expected: user.email,
});
});
It fails with:
$ SWC_NODE_PROJECT=./jsconfig.json riteway -r #swc-node/register src/features/home/home-page-component.test.js | tap-nirvana
/Users/janhesters/dev/my-project/src/features/home/home.module.css:1
(function (exports, require, module, __filename, __dirname) { .container {
^
SyntaxError: Unexpected token '.'
when we leave in the CSS import in home-page-component.js, or with:
$ SWC_NODE_PROJECT=./jsconfig.json riteway -r #swc-node/register src/features/home/home-page-component.test.js | tap-nirvana
node:internal/modules/cjs/loader:936
throw err;
^
Error: Cannot find module 'user-profile/user-profile-factories'
Require stack:
- /Users/janhesters/dev/my-project/src/features/home/home-page-component.test.js
- /Users/janhesters/dev/my-project/node_modules/riteway/bin/riteway
respectively, when we get rid of the CSS import.
How can we help SWC understand CSS (or mock CSS modules) and how can we help it understand absolute imports?
We already set the baseUrl in jsconfig.json ...
About absolute path
You already add baseUrl in the jsconfig.json file but didn't add the paths, you should modify your config file like mine:
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"#screens": ["./screens"],
"#shared": ["./shared"],
"#shared/*": ["./shared/*"]
},
The paths are the alias of module-resolver, and I guess your root shouldn't be ".", it should be exactly like your jsconfig.json file, I mean the baseUrl value.
"plugins": [
[
"module-resolver",
{
"root": ["./src"],
"extensions": [".ts", ".tsx", ".js", ".jsx", ".json"],
"alias": {
"#screens": "./src/screens",
"#shared": "./src/shared"
}
}
]
],
If you have Webpack, you should have alias config in your resolve key off Webpack config object, like mine:
const resolve = {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
alias: {
'#screens': path.join(__dirname, 'src/screens/'),
'#shared': path.join(__dirname, 'src/shared/'),
},
modules: ['src', 'node_modules'],
descriptionFiles: ['package.json'],
};
About CSS
Actually, you are using CSS file as CSS-Modules not like recommended NexJS doc, in docs developer should import CSS like import './styles.css' and then use it as string in JSX like <div className="main"
But
You are importing it like a module (CSS-Module):
// you did it
import styles from './styles.css';
<div className={styles.main}
As you know it is built-in support by this doc, it is supported by NextJS, but SWC cannot understand it. I put in hours to find a way for it, but it seems SWC doesn't support CSS-Module yet. you should create your own plugin for SWC to support CSS-Module.
How can we help SWC understand CSS (or mock CSS modules)? - SWC doesn't understand css natively, and neither did Babel. As you noted, when you were using Babel, the plugin styled-components took care of this. You'll need to do the same with SWC. I can't find an existing SWC plugin that does this, but you can roll your own. Obviously this is a pain, but such is the cost of using new tooling.
How can we help SWC understand absolute imports? - The .swrc options for baseUrl and paths should do what you want, but that, too, seems to have some issues.
You may have better luck creating issues directly in the #swc-node GitHub repo, but given the comments there it feels like you might be SOL for a while. Might be faster/easier to rewrite your tests using one of the libraries that Next supports out of the box.
I was able to solve all issues without writing any plugins. I pushed the solution to my example repo from the question.
Firstly, use the official SWC project's register. This means, you have to compile your tests like this:
"test": "riteway -r #swc/register 'src/**/*.test.js' | tap-nirvana",
You can install it by running:
yarn add --dev #swc/core #swc/register
We need this version of register, because it takes into account an .swcrc file, which the other one did not.
Secondly, you need both "path" and "baseUrl" to fix the absolute imports because for some reason SWC doesn't infer all paths with only a "baseUrl" provided. And you need to adapt your .swcrc to handle React.
{
"jsc": {
"baseUrl": "./src",
"paths": {
"*.css": ["utils/identity-object-proxy.js"],
"utils/*": ["utils/*"]
},
"parser": {
"jsx": true,
"syntax": "ecmascript"
},
"transform": {
"react": {
"runtime": "automatic"
}
}
},
"module": {
"type": "commonjs"
}
}
Thirdly, to solve .css you need to remap all imports to .css files to an identity object proxy, which you can see in the .swcrc example above. An identity object proxy is an object that, when you reference any property, returns the stringified key that you're trying to reference. You can create one yourself like this:
const identityObjectProxy = new Proxy(
{},
{
get: function getter(target, key) {
if (key === '__esModule') {
return false;
}
return key;
},
},
);
export default identityObjectProxy;
For the .css remap to go into effect you need to make all your imports to .css files absolute imports. (import styles from './styles.module.css won't work!)
I have a typescript library consists of multiple folders. Each folder contains an index.ts file which exports some business logic. I am trying to bundle this with rollup to achieve this behavior on the call site:
import { Button, ButtonProps } from 'my-lib/button'
import { Input, Textarea } from 'my-lib/input'
import { Row, Column } from 'my-lib/grid'
This is the directory structure:
I have a main index.ts under src/ which contains:
export * from './button';
export * from './input';
export * from './grid';
With this style, I can do:
import { Button, Input, InputProps, Row, Column } from 'my-lib'
But I don't want this. I want to access to each module by their namespaces. If I remove exports from the index.ts file, all I can do is:
import { Button } from 'my-lib/dist/button'
which is something I didn't see before. Adding dist/ to the import statement means I am accessing the modules via a relative path. I want my-lib/Button.
I am using rollup. I tried to use alias plugin but didn't work. Below is my rollup config:
const customResolver = resolve({
extensions: ['ts'],
});
export default {
input: `src/index.ts`,
output: [
{
file: pkg.main,
format: 'cjs',
sourcemap: true,
// plugins: [terser()],
},
{
file: pkg.module,
format: 'es',
sourcemap: true,
plugins: [terser()],
},
],
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
external: [],
watch: {
include: 'src/**',
},
plugins: [
// Allow json resolution
json(),
// Compile TypeScript files
typescript({ useTsconfigDeclarationDir: true }),
// Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs)
commonjs(),
// Allow node_modules resolution, so you can use 'external' to control
// which external modules to include in the bundle
// https://github.com/rollup/rollup-plugin-node-resolve#usage
resolve(),
// Resolve source maps to the original source
sourceMaps(),
alias({
entries: [
{ find: 'my-lib/button', replacement: './dist/button' },
{ find: 'my-lib/input', replacement: './dist/input' },
{ find: 'my-lib/grid', replacement: './dist/grid' },
],
customResolver,
}),
],
};
And this is the tsconfig file:
{
"compilerOptions": {
"target": "es5",
"module": "ES6",
"lib": ["ES2017", "ES7", "ES6", "DOM"],
"declaration": true,
"declarationDir": "dist",
"outDir": "dist",
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"allowJs": false,
"moduleResolution": "node",
"resolveJsonModule": true,
"baseUrl": "./src",
"paths": {
"my-lib/button": ["./src/button"],
"my-lib/input": ["./src/input"],
"my-lib/grid": ["./src/grid"]
}
},
"exclude": ["node_modules", "dist", "**/*.test.ts"],
"include": ["src/**/*.ts"]
}
I don't know how to achieve the same structure as lodash/xxx or material-ui/yyy with rollup.
People suggest aliases or named exports but I couldn't make it work.
The closest thing to my problem is below question:
Import from subfolder of npm package
I want to achieve the same thing but with typescript and rollup.
I think I am missing something, thanks.
This is possible, but requires some extra steps. A mentioned above, this is the approach taken by Material-UI.
The trick is to publish a curated dist folder, rather the root folder of your repo.
Building
To begin with, let's just be clear that it doesn't matter whether your library is built using CommonJS or ESM. This is about module resolution.
Let's assume the project is called my-package.
Now most projects, after we have built src/ to dist/ will have
my-package
package.json
src/
index.js
dist/
index.js
and in package.json
"main": "dist/index.js"
or for esm
"module": "dist/index.js"
Publishing
Most projects just add .npmignore and publish the root of the project, so when installed the project ends up in node_modules like so:
node_modules
my-package/
package.json
dist/
index.js
Resolving
Once installed, consider this import:
import myProject from "my-project";
The module resolver will do this (simplifying greatly, as the full algorithm is irrelevant here):
Go to node_modules
Find my-project
Load package.json
Return the file in main or module
Which will work because we have
node_modules
my-package/
package.json
dist/
index.js
Resolving subpaths
import something from "my-project/something";
The resolution algorithm will work with
node_modules
my-project/
somthing.js
also with
node_modules
my-project/
something/
index.js
and with
node_modules
my-project/
something/
package.json
where in the latter case it will again look at main or module.
But we have:
node_modules
my-package/
package.json
dist/
index.js
The Trick
The trick is, instead of publishing your project root with its dist folder, to "frank" the dist folder and publish the dist folder using npm publish dist instead.
Frank (as in frank a letter) means you need to create a package.json in your dist folder; add README.md LICENSE etc.
A fairly short example of how this is done can be found here.
So, given we had after build:
node_modules
my-package/
package.json
dist/
index.js
something.js
Once published we get
node_modules
my-project/
package.json
index.js
something.js
Where package.json is the curated one.
First of all, the only difference between
import { Button } from 'my-lib/dist/button'
and
import { Button } from 'my-lib/button'
is just one more directory level.
Once said that, until you have "outDir": "dist", in your tsconfig.json file you need to add dist/ to your import statements.
Indeed, both the libraries you taken as example are distributed with files in the root directory: lodash directly has js files in the root, while material-ui has not outDir option in its tsconfig.json file (which means to write output files to root directory).
Hope this helps.
After numerous trials and errors, I was able to get this working by passing in a list of inputs, using the preserveModules and preserveModulesRoot options, and a simple postinstall script.
Here's my rollup.config.js
const options = {
input: [
'src/index.ts',
'src/api/index.ts',
'src/components/index.ts',
'src/contexts/index.ts',
'src/hooks/index.ts',
'src/lib/index.ts',
'src/types/index.ts',
'src/utils/index.ts',
'src/UI/index.ts',
],
output: [
{
format: 'cjs',
dir: 'dist',
exports: 'auto',
preserveModules: true,
preserveModulesRoot: 'src',
sourcemap: true,
},
],
plugins: [
// Preferably set as first plugin.
peerDepsExternal(),
typescript({
tsconfig: './tsconfig.rollup.json',
}),
postcss({
extract: false,
modules: true,
use: ['sass'],
}),
],
};
export default options;
scripts/postinstall.sh
#!/usr/bin/env bash
set -e;
# skip postinstall if npm install for development
# rollup.config.js is not included in dist
if [ -f "rollup.config.js" ]; then
echo "skipping your package's postinstall routine.";
exit 0;
fi
echo 'Copying files from dist folder into root project folder...'
cp -r dist/* ./ && rm -rf dist
echo 'Postinstall done!'
package.json
"scripts": {
"postinstall": "./scripts/postinstall.sh",
},
This will compile and output all files to dist folder. The postinstall script will copy all files from dist into the root project folder.
Note*: The postinstall script should be skipped when running npm install locally. This is done by checking if rollup.config.js exists or not.
I'm trying to setup yarn workspaces with my docker instance. This is my directory structure:
/monorepo/
/node_modules/
#libs/common
#services/common
#services/project-A
...OTHER DEPS...
package.json
/services/
/common/
index.jsx
package.json
/project-A/
webpack.base.config.js
**REACT project with babel, webpack, etc**
/libs/
/tools/
/common/
index.jsx
package.json
To simplify my docker setup I just configured this volume within my docker compose that maps the entire monorepo directory:
volumes:
- '../../../monorepo:/monorepo'
From there in my Project-A I import #libs/common and #services/common. This works fine when the common libraries are exporting simple functions like:
export const Add = (a,b) => a+b
Webpack has no issue resolving this and building Project-A.
However when I try to import a component from one of the common libraries like this:
/libs/tools/common:
import React from 'react'
export MySharedComponent = () => <>HELLLO</>
I get an error in the build process:
Error: Cannot find module '/monorepo/libs/tools/common/webpack.base.config.js'
Require stack:
- /monorepo/node_modules/eslint-import-resolver-webpack/index.js
- /monorepo/node_modules/eslint-module-utils/resolve.js
- /monorepo/node_modules/eslint-plugin-import/lib/rules/no-unresolved.js
- /monorepo/node_modules/eslint-plugin-import/lib/index.js
The eslint file under Project-A:
{
"parser": "babel-eslint",
"env": {
"browser": true,
"node": true,
"jest": true,
"cypress/globals": true
},
"settings": {
"import/resolver": {
"webpack": {
"config": "webpack.base.config.js"
}
}
}
}
The babel.rc under Project-A
{
"presets": [
[
"#babel/preset-env",
{
"targets": {
"node": "current"
}
}
],
"#babel/preset-react",
"#babel/preset-flow"
],
"env": {
"test": {
"plugins": [
[
"babel-plugin-webpack-alias",
{
"config": "./webpack.base.config.js"
}
]
]
}
}
}
My Question:
Is the main issue that there's no webpack config set up in the common repositories. Therefore the workspace does not know how to compile my shared resources?
Should there only be 1 webpack build config in my workspace used by all projects within the workspace? Currently I only have 1 config under Project-A?
What happens if I have specific webpack needs per project, does 1 config (if that's the answer) make sense?
1) First, there is a line in your code that you are referring to webpack.base.config.js in both babelrc and eslint,
so if that file does not exist, this error that says module not found makes sense.
2) Second: if you build and use your repositories in the same situation and environment, yes you can have one config for both but you might need environment setup (Development, Production) for your config.
But if you really want to make your dependencies and configs apart, webpack support multiple entries for your project which you can check that out.
How can I compile my Typescript project into a TS compilation so I can then import it into other TS projects and use the type definitions? For backward compatibility, I also want to export them as pure JS too so others who don't use TS, can still use the project.
You need to publish your project as an npm package. You can create a private package on npm if you want (but you need a paid account for that), or you can publish it publicly, or you can use sinopia, which is basically a local instance of npm.
Any one of these options requires you to have an up to date package.json file that specifies your project's dependencies.
You will be publishing your package in compiled form. So, if you specify your tsconfig and package.json properly, you will be exporting js files along with d.ts. files and the package will be usable either by typescript or vanilla javascript.
This is how I've done it:
// package.json
// The property name: #my-org/... means that the package is scoped -
// you can point a #scope at a specific NPM registry.
// See https://docs.npmjs.com/misc/scope
// We use myget.org to host our private packages.
{
"name": "#my-org/ng-lib",
"version": "1.0.8",
"main": "dist/index.js",
"scripts": {
"transpile": "tsc --outDir ./",
"clean": "rimraf ./services && rimraf ./*.js && rimraf ./*.d.ts"
},
"author": "*** <me#my-org.com>",
"license": "ISC",
"files": [
"**/*.js",
"**/*.d.ts"
],
"typings": "index.d.ts",
"dependencies": {
"#types/angular": "^1.5.20",
"angular": "^1.5.9"
},
"devDependencies": {
"rimraf": "^2.5.4"
}
}
EDIT:
Notice the "typings": "index.d.ts" line? That describes the main "types" file for the package. So when you do an import * as ngLib from '#my-org/ng-lib' it will use the typings from node_modules/#my-org/ng-lib/index.d.ts for intellisense, and upon transpile webpack will find the main js file at node_modules/#my-org/ng-lib/dist/index.js
So if you've created an index.d.ts by hand and all you've got to export are interfaces you can point the typings field at that index.d.ts as interfaces have no implementation, and just describe the shape of an object.
However, if you've got objects with logic (methods, getters/setters, etc) they will more than likely be classes, which you'll need to transpile down to .js AND .d.ts files.
EXAMPLE:
./index.ts # Re-exports both my-class and my-interface
./my-class.ts # Implements my-interface.d.ts
./my-interface.d.ts
This output of this after transpile should be as follows in a "typed" NPM package:
./index.d.ts
./index.js
./my-class.js
./my-class.d.ts
./my-interface.d.ts
And package.json will include the following lines:
"main": "./index.js",
"typings": "./index.d.ts",
"files": [
"./index.d.ts",
"./index.js",
"./my-class.js",
"./my-class.d.ts",
"./my-interface.d.ts"
]
... and when consuming the package (once it's published and installed in another project) can be done in the following ways:
import * as ngLib from '#my-org/ng-lib'
import { MyClass } from '#my-org/ng-lib'
import { MyInterface } from '#my-org/ng-lib'
import { MyClass } from '#my-org/ng-lib/my-class'
import { MyInterface } from '#my-org/ng-lib/my-interface'
END EDIT
The declaration property in tsconfig.json will emit .d.ts files describing the 'shape' of your exported objects.
// tsconfig.json
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"sourceMap": false,
"emitDecoratorMetadata": true,
"declaration": true,
"experimentalDecorators": true,
"removeComments": true,
"noImplicitAny": false,
"suppressImplicitAnyIndexErrors": false,
"baseUrl": "./src",
"listFiles": true,
"noImplicitUseStrict": false
},
"exclude": [
"node_modules"
]
}
And if it's a complicated package, I'll have separate modules and aggregate them in the index.ts like so:
// index.ts
export * from './module-one'
export * from './module-two'
export * from './module-three'
You can also have sub-directories, each with their own indexes.
This will create both the index.js and index.d.ts files, which allows the following:
import * as ngLib from '#my-org/ng-lib'
import { ModuleOne } from '#my-org/ng-lib/module-one'
import { SubModuleOne } from '#my-org/ng-lib/submodules/submodule-one'