Creating your own NPM package
Tags: react, typescript, npm
Published: 2022-02-19 19:50:04

#What is a npm package

NPM stands for Node package manager which is a software registry for Open source software where users can publish packages for public and private uses alike.

**Managing packages inside a npm package**

A npm package is normally a basic application which uses other packages to manage and use basic functionalities. But in normal case scenario we use yarn or npm to install these packages but while creating a npm package we need a global way to store and manage packages across the entire project. So for this I used LERNA, the official documentation for lerna can be found [here](https://lerna.js.org/).

This is the basic folder structure for managing lerna projects ![lerna folder structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hqrdgdcg0rvsi5r27u33.png)

The folder has following dependencies :-
- cli (Managing the command line interface)
- local-API (The back-end API built upon express)
- local-client (The UI interface uses React, redux and bulma for styling)

###Let us first look at CLI 

For CLI, I used a package called `commander` in which you write code to describe your command line interface. Commander looks after parsing the arguments into options and command-arguments, displays usage errors for problems, and implements a help system for unrecognized options it displays an error.

The official documentation to commander can be found [Here](https://www.npmjs.com/package/commander).

The commander takes in a command and some options, in this case the command is serve and option is a port number where this program runs which by default is 4005.

``` javascript
const serveCommand = new Command()
  .command('serve [filename]')
  .description('Open a file for editing')
  .option('-p --port <number>', 'port to run server on', '4005')
  .action(async (filename = 'notebook.js', options: { port: string }) => {
    try {
      const dir = path.join(process.cwd(), path.dirname(filename));
      await serve(
        parseInt(options.port),
        path.basename(filename),
        dir,
        !isProduction
      );
      console.log(
        `Opened ${filename}. Navigate to http://localhost:${options.port} to edit the file.`
      );
    } catch (error: any) {
      if (error.code === 'EADDRINUSE') {
        console.error('Port is already in use please try another port');
      } else {
        console.log(error.message);
      }
      process.exit(1);
    }
  });

```
Besides this the following dependencies are also used in the cli package

![cli package](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7nf0bgpr98a5y9wiy9d2.png)

In the local api directory, all the routes are defined, it basically has two routes:- 

- A get route to `/cells` (This endpoint returns the existing cell data from the notebook file)

```javascript
router.get('/cells', async (req, res) => {
    try {
      const result = await fs.readFile(fullPath, { encoding: 'utf-8' });
      res.send(JSON.parse(result));
    } catch (error: any) {
      if (error.code === 'ENOENT') {
        await fs.writeFile(fullPath, '[]', 'utf-8');
        res.send([]);
      } else {
        throw error;
      }
    }
  });
```
At first we are trying to read the existing contents of the file using the file system(fs) inbuilt module and since the data is in JSON format we are parsing it and sending it back.

Wrapping the entire code in a try-catch block makes it easier to send errors rather than crashing the app.

- A post route to `/cells` (This endpoint sends the existing cell data to be saved into notebook file)

```javascript
router.post('/cells', async (req, res) => {
    const { cells }: { cells: Cell[] } = req.body;
    await fs.writeFile(fullPath, JSON.stringify(cells), 'utf-8');
    res.send({ status: 'ok' });
  });
```
Similarly in the post route we are getting the data from the client converting it into a JSON string and writing it back using the same file system(fs) module.

You can find more about FS modules [here](https://www.npmjs.com/package/fs).

Finally comes the client module which is built using React, redux, typescript, bulma and monaco editor.

For this the main challenges were:- 

- Building a markdown editor
- Building a solution for writing & compiling code online in the browser itself.
- Building a bundler for compilation.

For the markdown editor I ended up using `@uiw/react-md-editor`.

```javascript
import { useState, useEffect, useRef } from 'react';
import MDEditor from '@uiw/react-md-editor';
import './css/text-editor.css';
import { Cell } from '../state';
import { useActions } from '../hooks/use-actions';

interface TextEditorProps {
  cell: Cell;
}

const TextEditor: React.FC<TextEditorProps> = ({ cell }) => {
  const [editing, setEditing] = useState(false);
  const ref = useRef<HTMLDivElement | null>(null);
  const { updateCell } = useActions();

  useEffect(() => {
    const listener = (event: MouseEvent) => {
      if (
        ref.current &&
        event.target &&
        ref.current.contains(event.target as Node)
      )
        return;
      setEditing(false);
    };

    document.addEventListener('click', listener, { capture: true });

    return () => {
      document.removeEventListener('click', listener, { capture: true });
    };
  }, []);

  if (editing) {
    return (
      <div className="text-editor" ref={ref}>
        <MDEditor
          value={cell.content}
          onChange={(v) => updateCell(cell.id, v || '')}
        />
      </div>
    );
  }

  return (
    <div className="text-editor card" onClick={() => setEditing(true)}>
      <div className="card-content">
        <MDEditor.Markdown source={cell.content || 'Click to edit'} />
      </div>
    </div>
  );
};

export default TextEditor;

```
To read more about `@uiw/react-md-editor` you can go [here](https://www.npmjs.com/package/@uiw/react-md-editor).

Now for writing and compiling code online I needed a code editor which looks and feels like VS-Code and so ended up using monaco editor which is created by microsoft itself and powers VS code also.

This is the configuration i used for my editor component:-
```javascript
<MonacoEditor
  editorDidMount={onEditorMount}
  value={initialValue}
  height="100%"
  language="javascript"
  theme="dark"
  options={{
    wordWrap: 'on',
    matchBrackets: 'always',
    minimap: { enabled: false },
    showUnused: false,
    folding: false,
    lineNumbersMinChars: 3,
    fontSize: 18,
    scrollBeyondLastLine: false,
    automaticLayout: true,
  }}
/>
```
Now after creating the editor there were 2 more issues:-
1. The code was not formatted properly.
1. And there were some highlighting issues.

To fix code formatting i created a button which calls the prettier package to format code.

```jsx
 const onFormatClick = () => {
    const unFormatted = editorRef.current.getModel().getValue();
    const formatted = prettier
      .format(unFormatted, {
        parser: 'babel',
        plugins: [parser],
        useTabs: false,
        semi: true,
        singleQuote: true,
      })
      .replace(/\n$/, '');

    editorRef.current.setValue(formatted);
  };

<button onClick={onFormatClick}>
  Format
</button>
```
Then for code highlightiong i used `jscodeshift` and `monaco-jsx-highlighter`and created a mount component which ran when editor mounted:-

```javascript
 const onEditorMount: EditorDidMount = (getValue, monacoEditor) => {
    editorRef.current = monacoEditor;
    monacoEditor.onDidChangeModelContent(() => {
      onChange(getValue());
    });

    monacoEditor.getModel()?.updateOptions({ tabSize: 2 });

    const highlighter = new Highlighter(
      // @ts-ignore
      window.monaco,
      codeshift,
      monacoEditor
    );
    highlighter.highLightOnDidChangeModelContent(
      () => {},
      () => {},
      undefined,
      () => {}
    );
  };

```
Then comes the most important part **_The bundler_**:-

For bundling, the basic use-case is we need to get the code, compile it and then show the output. Now what if an user imports some packages from npm registry? 
For that reason we would need a bundler and in my case i used unpkg and created a bundler service.

```javascript

import * as esbuild from 'esbuild-wasm';
import { fetchPlugin } from './plugins/fetch-plugin';
import { unpkgPathPlugin } from './plugins/unpkg-path-plugin';

let service: esbuild.Service;
const bundle = async (rawCode: string) => {
  if (!service) {
    service = await esbuild.startService({
      worker: true,
      wasmURL: 'https://unpkg.com/esbuild-wasm@0.8.27/esbuild.wasm',
    });
  }

  try {
    const result = await service.build({
      entryPoints: ['index.js'],
      bundle: true,
      write: false,
      plugins: [unpkgPathPlugin(), fetchPlugin(rawCode)],
      define: {
        'process.env.NODE_ENV': '"production"',
        global: 'window',
      },
      jsxFactory: '_React.createElement',
      jsxFragment: '_React.Fragment',
    });

    return { code: result.outputFiles[0].text, err: '' };
  } catch (err) {
    return { code: '', err: (err as Error).message };
  }
};

export default bundle;

```

##Putting it all together
After this its time to deploy it to npm registry, now for that we would need to create a npm account which is pretty much straight forward and can be easily done by going to npm [website](https://www.npmjs.com/) and signing up.

Now we need to make some changes in our package.json file.

We need to add the main, types(if its a typescript file) and license (mostly MIT for OSS)

![first step](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uahb8fvvau56jkhutbwf.png)

Now add the publishConfig to be public or private and the entry folder from where npm serves.
![step2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/41g8n78tntck6c7qptkl.png)

That's it you are good to go...
[Here](https://github.com/dpak1999/codeoc) is the entire source code for the project.

Do check it out and leave a star..