Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Importing from subfolders for a javascript package

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:

enter image description here

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.

like image 499
alioguzhan Avatar asked Jun 22 '20 15:06

alioguzhan


3 Answers

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.

like image 159
Izhaki Avatar answered Oct 02 '22 15:10

Izhaki


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.

like image 40
Daniele Ricci Avatar answered Oct 02 '22 17:10

Daniele Ricci


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.

like image 36
TrieuNomad Avatar answered Oct 02 '22 16:10

TrieuNomad