how to set dependencies and how to compile an npm module? - javascript

I have a few npm modules published, all modules for existing libraries, like three.js or react.
The packages seem to be downloaded but i've received no feedback on whether it's done right or not.
Dependencies
What is the high level goal when defining dependencies?
three.js:
This is confusing because every "extension" just assumes that there is a THREE object available in some context.
My three.js module thus only mentions:
"devDependencies": {
"three": "^0.88.0"
}
And it's being used like this:
require( 'three-instanced-mesh' )(THREE)
Which both makes sense and doesn't.
The module can't work without three.js and a proper context passed in (THREE), but since i pass it in at runtime(?) it doesn't seem like it is an actual dependency. When I checkout the repo and want to develop in it, i do need to install three.js if i want the code to run.
React
I've published a React component which i intended to be used as such:
npm install my-module
import MyModule from 'my-module
<MyModule/>
For some reason I listed react as a peerDependencies dep.
<MyModule/> in JSX would imply that I've done something to have react already available in this context (similar to how THREE is passed in first example?).
The difference here is that i don't define the class at runtime, and thus calling import MyModule requires react to be available in MyModule.js?
What is the desired goal here and how to describe it? I only know that i don't want npm install my-module to install a different version of react, or to cause more react somehow to be bundled in the final bundle (but i'm not even sure about that).
What type of dependency (if any) should react be to my-react-component and how would i actually link it to my module?
For example using the externals thing with webpack vs having an actual import React from 'react'?
Build
If i set up my repo to work with the latest and the greatest of JS (or not even JS?), how and what should i publish?
import Foo from 'foo' //<-- where does 'foo' point and what is 'foo'?

What is the high level goal when defining dependencies?
You have to define which dependencies you are using only when developing (devDependencies) and the ones that are needed when someone installs your package and are going to be installed automatic (dependencies), and dependencies you need to be available, but you want the user to install (which, honestly, does not makes sense) peerDependencies.
The difference here is that i don't define the class at runtime, and thus calling import MyModule requires react to be available in MyModule.js?
It would require React to be available where the file is being imported, i.e: A imports myModule, but A has to have react imported. Putting as peer dependencies is the best way here indeed.
For example using the externals thing with webpack vs having an actual import React from 'react'?
Using externals in webpack just tells webpack to not bundle react and says that react will have been imported before the import of this component.
If i set up my repo to work with the latest and the greatest of JS (or not even JS?), how and what should i publish?
Usually the index.js file that contains the library minified/bundled. Publish that with npm, you'll need to setup main field on package.json
import Foo from 'foo' //<-- where does 'foo' point and what is 'foo'?
foo points to the name of the package that you created, i.e: the name this package was publish under. When you go to npmjs.org and search for foo, that is going to be the package. foo is in your node_modules.

Related

According to whether the package exists in the window or not load the package dynamically

Does any build tool support this loading strategy, such as webpack or rollup.js.
Build every dependency to a single bundle, and when loading these dependencies, firstly search it in window['package'], if exist, use it. Otherwise dynamic load dependencies bundle to use.
Such app dependency is React, ReactDOM and UiLib.
The built result is:
React -> a.js
ReactDOM -> b.js
UiLib -> c.js
my code -> d.js
if window.React exist but window.ReactDOM and window.UiLib does not exist. d.js should dynamically load b.js and c.js and use window.React.
I know I can config React to externals, but this is a microapp used in many different apps, I'm not sure which packages exist in every global.
Nope. It is not possible directly. For a bundler, it is a binary choice between bundle or not to bundle.
Why? When a bundler encounters a library via import statements like - import React from 'react', it needs to know what global object it should substitute whenever it encounters react package across the entire application dependency graph. This must happen at compile-time. Additionally, loading a library with dynamic decision at runtime means you are introducing an asynchronous behavior in your code which your components or application cannot handle readily.
There are two form factors - a library and application. As far as library is considered, this is the only way to teach bundler (either bundle it or leave it via externals).
At an application level, you can write your own code to partially achieve what you seek with help of CDN. For this, you use externals and tell Webpack, for example, that react will be available as global React object on window namespace.
Now before your library is getting consumed, you have to add a dynamic code where to check for presence of React object.
function async initialize() {
if (!window.React) {
const React = await import(/* webpackIgnore: true */ 'https://unpkg.com/react#18/umd/react.development.js')
window.React = React;
initializeMicroapp();
} else {
initializeMicroapp();
return Promise.resolve();
}
}
Your initialize function for microapp is async and returns a promise. This is usually the pattern to go ahead with shell + micro-frontends.
On a side note, you can use module federation approach which is actually meant to solve exactly similar use-case. With module federation, you can teach Webpack that if the host/shell provides a library, then Webpack should simply ignore its bundled copy of that library while serving only other necessary code. However, I advice caution as it is a very specific pattern and neither de-facto nor de-jure at this point. It is recommended when you are having sufficient scale and many independent teams working on same product space.

Problems using "index.js" to import ES6 JS Modules

I'm working with React Native and using index.js to manage modules. I have many projects consuming from the same components folder, which has a structure like this:
components
|_ComponentOne.js
|_ComponentTwo.js
|_index.js
In which the index.js looks like this:
export * from './ComponentOne.js';
export * from './ComponentTwo.js';
Now lets say I have three projects:
ProjectOne, which uses ComponentOne;
ProjectTwo, which uses ComponentTwo;
ProjectThree, which uses both;
Every project has its own files, but they refer to this same folder to use components (like a shared assets folder). Everything works fine while all the projects have all dependencies for all components.
In another words, I have a problem when one of the projects doesn't install a dependency for one of the components, even when the project doesn't uses that component.
Let's take as an example ProjectOne, which uses only ComponentOne. If ComponentTwo (which is not used in this project) has dependency X, I have to npm install dependency X even on ProjectOne, or an error is given. Again, ProjectOne doesn't use dependency X.
I can only image this happens because the index.js validates all declared exports, even when they're not used.
I'm trying to find an alternative to not be forced to install plugins and other things that I won't even use in my projects. I know that if I remove the index.js and start importing files directly on projects, it will work, but I would like to keep the index.js structure (to be able to use multi import syntax import { ComponentOne, ComponentTwo } from 'components').
Any suggestion?
Update:
The error I get, when I do not npm install dependency X is
Module `X` does not exist in the Haste module map
If I install it, everything works.
I'm using the terminal to install the application directly into an android phone. The JS bundle is automatically created by Metro (RN default).

How can I exclude an ES6 module (VueJS) from treeshaking in RollupJS? (and should I?)

Right now I pull in all my own es6 modules and create a bundle using Rollup.
Recently I started using VueJS, which now has an ES6 Module which can be pulled in just like my own modules. Rollup does some treeshaking on it, but I don't know if that is a very good idea? I don't know what it is doing, so I would rather it does nothing!
Instead I just add vue at the end of my HTML:
<script src="dist/bundle.js"></script>
I love the convenience of having everything as one bundled file, but should I really treeshake the entire Vue app, is there a command in Rollup that I can not treeshake just this one module?
EDIT
I have found the --external option, which seems good as it would just keep the import for vue and bundle the rest, but it does not seem to work!
When I use rollup --format=iife --external=../node_modules/vue/dist/vue.esm.browser.js --file=dist/bundle.js -- src/main.js it says Error: Could not resolve '../node_modules/vue/dist/vue.esm.browser.js' from src/app.js.
In my main.js it has import Vue from '../node_modules/vue/dist/vue.esm.browser.js; which works fine for the app. I want to make Vue an external, but it won't work!
To prevent Rollup from treeshaking a particular module, you can simply import it blindly (instead of a part of it), so that Rollup thinks the module performs some side effect:
import 'vue'
Of course you can still import some bits in parallel, so that you can rename the default export for example:
import 'vue'
import Vue from 'vue'
As for your --external option, you probably just need to wrap the path value with quotes:
--external='../node_modules/vue/dist/vue.esm.browser.js'
Note that you should probably switch to Rollup configuration file (instead of CLI options) to make your life easier. You will also be able to use rollup plugins, e.g. rollup-plugin-alias to manage the exact location of the Vue file you want to use.

What is a "deep import" in Angular 4 context?

I know I have to remove all deep imports before upgrading to Angular 4, but I have no idea what is a deep import. Literally nobody mentions it. What is it? How does it look like?
Taken from How to deal with losing deep imports in Angular 4, an example of a deep import:
import { VALID } from '#angular/forms/src/model'
meaning 3 levels deep, whereas now you can only go 1 level:
import { VALID } from '#angular/forms'
which is invalid if "VALID" is in model, 3 levels deep. that's all there is to it. If you need something that's in "deep", it should be exported to in the first level now, or you need to open a ticket to angular to export it.
Not trying to advertise my own gist article, but I explain them here
Description
A "deep import" is simply an ESM import that goes deeper than the package root:
import thingA from 'my-package-name/src/components/thingA'
import thingB from '#my-namespace/my-package-name/src/components/thingA'
A namespaced package does not necessitate a deep import if the package name (package.json:name key) contains a slash:
import thingA from '#my-namespace/my-package-name'
Relative imports (./path/to/module) are also not considered deep imports, in fact it might be considered bad practice to "shallow import" an exported module from one's own package by it's package.json:main key location (i.e. src/index.js) as this can often cause circular dependencies.
Why deep imports can be a problem
Package bundling
Package bundlers are often used to transpile and condense a project to a distributive, single-file version (i.e. dist/bundle.js). Other packages that try to deep import from such packages experience errors, as the directory structure from src may often not be maintained or even included in the published package.
Unintended imports and directory changes
A developer might deep import a module from a package when it was not intended to be used outside of the scope of that package. Your changing of the name of an internally-used export or your package's directory structure should not require a major version (breaking change) update for a module that was only intended to be consumed internally.

Which npm packages will and will not work on Angular 2? How do I tell?

Does an NPM package need to be modified to be compatible with Angular 2 (eg. add in typings, make directives for them) or will any existing package work? If they're not all compatible, how do I know what is and what is not compatible?
For example, say I want to import this package (https://github.com/pvorb/node-md5). I'm aware there is a ts-md5 package for angular 2 to do md5 - I'm just using this package as an example.
How would I get this to work?
I've installed it using
npm install md5 --save
npm install #types/md5 --save
but I can't seem to be import it
import {md5} from 'md5';
I get this error message after I try to run
Module
'"/Users/xxx/Source/tempProjects/ngUnderscore/node_modules/#types/md5/index"'
resolves to a non-module entity and cannot be imported using this
construct.
I'm not sure what this message means. Does it mean that in its current state, the package is not compatible or am I trying to use the package incorrectly?
I managed to make it work using declare and require instead of import (import won't work for this non-module lib)
declare const require: any;
const md5 = require('md5');
If you don't want to workaround import like this, you can try using Typescript MD5 implementation called ts-md5. Then import like the one below should work. (referenced from this question)
import { Md5 } from 'ts-md5/dist/md5'
If there is no TS implementation, you can search for the types in DefinitelyTyped and then simply install package by npm i --save-dev #types/{package-name}
If the library works on your project depends on many factors: your Angular version, your TypeScript version, etc.
This said, is obvious that we should check the library's documentation and see which dependencies has and its versions, and of course the library should be the Angular 2 version of itself. Following your example, there are several md5 libraries but if you use TypeScript you should maybe consider this one: https://www.npmjs.com/package/ts-md5
If we have all that covered but still there is something not working because of some type of incompatibility, like for example:
My version of angular is 2, the library I just installed works with Angular 4. I have code full of <template>, library uses <ng-template>... What can I do?
You can fork the library in github and modify whatever you need to asure it is compatible with your project. Steps:
Fork library repository and modify what you need
Subscribe to main library repository in order to be updated with changes
In packages.json use your forked version of the library, for example:
"ng2-datetime": "https://github.com/my_user/ng2-datetime/tarball/my_forked_version_name",
If you think that your modifications could suit other users... Make a Pull request! :D
This is more of a TypeScript question since md5 is not an Angular package.
The key is to get the import correct to be equivalent to a require() function.
import * as md5 from "md5";
Use directly in TypeScript file:
console.log(md5('message'));
To use this on the template, preferably it should be used in method implementation, but can also be exposed directly. Add it as a property on the Component:
md5: any = md5;
Then on the template:
{{md5('message')}}
They usually say which Angular it is meant for, sometimes you have one package for both or for each.
If you are using an Angular 1x package and there is no Angular2 compatibility, then you can just use ngUpgrade.
But If you are using a common plugin then there must be an angular 2 solution.
If you want the other way around then you're probably going the wrong way.
The issue you experienced is not related to Angular. It is an existing issue on TypeScript when importing CommonJS packages.
The rule of thumb (my recommendation) is to stay away from using the interop feature (i.e. import * as X from 'x') when importing CommonJS and use the "old" syntax instead (i.e. import X = require('x')).
When CommonJS is exporting a function in the form of module.exports = function() {...}, import * as X from 'x' does not work.
This includes the case when the package is exporting an ES6 class but transpiling to ES5 with Babel.
You may see some packages do work with this syntax, but that is because the typings have a hack in it:
declare module 'x' {
function foo(): void
namespace foo {} // <-- the hack
exports = foo
}
There are other reasons the interop is not a good idea, including the syntax between TypeScript (import * X from 'x') and Babel (import X from 'x') does not agree.
You can learn more about it here and follow the links:
https://github.com/unional/typescript-guidelines/blob/master/pages/default/modules.md#import-keyword
UPDATE: with TypeScript#2.7 released, you can now do import EditableElement from 'Transformer' directly.
Turn on esModuleInterop in your tsconfig.json

Categories