jspm workflow for creating front-end libraries - javascript

What is the current recommended practice for converting a library written in TypeScript to ES5?
JSPM documentation seems to be geared toward web apps (i.e. with jspm bundle-sfx).
All the articles I can find on Google seems to be assuming a web app workflow rather than a library workflow.
My projects have the following setup:
It depends on react, flux and jquery, all installed through jspm and are properly configured in config.js
The source .tsx/.ts files are located in a src/ tree, along with their corresponding transpiled .js files
I am able to create a bundle with jspm bundle, however, this still requires the end user of my library to be using SystemJS
What I want is to bundle the entire tree under src/ into a single file without libraries such as react or jquery. How can I do this?
So far I've tried jspm bundle src/<MY_MAIN.js> - react - jquery <OUT.js> this works, but user still need a module loader to interact with exported symbols in MY_MAIN.js. I would also like to provide users with an option to manually import my library with <script> tags. self-executed bundles do not seem to work. No symbol is accessible globally once loaded through the <script> tag and I cannot exclude the framework code.

There are basically three approaches that I want to highlight, targeted at different end-user workflows
1. the <script/> tag approach
Here, create an entry .ts file that exports the main symbols of the library like so:
// Main.ts
import {MyLib} from "./components/MyLib";
import * as React from "react";
import * as ReactDOM from "react-dom";
/**
* browser exports
* note, typescript cannot use interfaces as symbols for some reason, probably because they are not emitted in the compiled output
* running `jspm bundle-sfx` results in the code here being accessible in <script /> tag
*/
(function (window) {
window.MyLib = MyLib;
window.React = window.React || React;
window.ReactDOM = window.ReactDOM || ReactDOM;
})(typeof window !== "undefined" ? window : {});
then run jspm bundle-sfx Main.js my-lib.sfx.js, the same works for browserify, except we have to use commonjs style require() instead of ES6 style import
2. Concat & Minify src files through regular gulp/grunt
this should just be the good old workflow we are all familiar with
3. Assume ES6 compatibility for apps that will use the library
Distribute the code as ES6/TS along with .d.ts and assume the user will also use jspm/system or eventual ES6 module loader approach to load your module

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.

Why do I need to add the .js extension when importing files in typescript but not in Angular imports?

In my typescript project I need to add the *.js extension when doing imports, otherwise the project will build but it will fail at runtime in the browser because it cannot find the file.
This is what my typescript imports look like:
import {MainApp} from './MainApp.js';
window.onload = () => {
var app = new MainApp(5);
app.doSomething();
};
From what I have read (Appending .js extension on relative import statements during Typescript compilation (ES6 modules) for example) it seems a normal thing for typescript that I cannot do this:
import {MainApp} from './MainApp.js';
But the thing is that in Angular using typescript I can do this:
import {MainApp} from './MainApp';
So, how it is Angular doing it? There is a way I can replicate that behavior in my non angular, pure typescript project?
Because angular cli first compiles all your source files into fewer files for the browser. All your code from multiple files lands in just one .js file. At compile time the compiler finds the MainApp related file and puts it into the output file.
Whereas the typescript compiler for the most part just removes the TS parts and keeps the TS parts. Otherwise it doesn't touch the files. The browser then at runtime requests all the source .js files.
If you don't want to care about file endings in import you'll need a bundler. There are many different ones like webpack, rollup, parcel, and many more.

How to use tree shaking for node modules with Webpack 5?

I want to boost my website performance. I recently see an error on unused javascript from lighthouse.
I checked the bundle and apparently those unused javascript are actually being used from other modules and node packages which I have been download.
For example, #sentry/node is what I'm using, but report shows unused javascript from #sentry/hub. But I only did install on #sentry/node but not the whole #sentry package. Further more, #sentry/node is using #sentry/hub, but I'm not importing #sentry/hub anywhere in my code (which I assume that causes the problem)
I have included "sideEffects": false to my package.json file but nothing seems to work
You could try destructuring, target only the object (or function) you are using.
e.g.
import { Component } from 'react';
Therefore, for you...
import { yourFunction } from `#sentry/node`;
Or simply take the code #sentry/node that you are using. 🤷‍♂️
Otherwise, show your code:
package.json
webpack.config.js
your "index" file (where you import your scripts and other files)
the file you import #sentry/node in

How to make TypeScript output valid ES6 module import statements?

All major browsers have supported ES6 modules for some time.
These differ from many of the server-side approaches in that they need to specify the exact file to import from - they can't use file discovery.
This makes sense - in Node applications or bundlers like WebPack they only really need the name of the module, and then can spend a bit of extra time discovering the specific file that holds the code. On the web that could be a lot of wasted round trips (is 'library' in library/index.js, or library/library.js, or library.js? require() doesn't care but on the web we have to).
TypeScript has ES6 modules support (set "module": "es6" in tsconfig.json) but it appears to be using a file discovery approach...
Suppose I have library.ts:
export function myFunction(...) { ... }
Then in app.ts:
import {myFunction} from './library';
var x = myFunction(...);
However, this is unchanged when transpiles - the TS output still has the 'library' name for file discovery, which doesn't work. This throws an error because 'library' isn't found:
<script type="module" src="app.js"></script>
In order for ES6 modules to work the TS output needs to reference the specific file:
import {myFunction} from './library.js';
var x = myFunction(...);
How do I make TS output valid ES6 module import statements?
Note: I am not asking how to make a bundler join the TS output into a single file. I specifically want to load these files individually using <script type="module">
This is a bug in TypeScript, though there's some debate about whether it should be fixed.
There is a workaround: while TS won't allow you to specify a .ts file as the source of a module, it will let you specify a .js extension (and then ignore it).
So in app.ts:
import {myFunction} from './library.js';
var x = myFunction(...);
This then outputs correctly in app.js, and TS has found the import definitions and bindings correctly.
This has one advantage/gotcha to be aware/careful of: TS just ignores the .js extension and loads the rest of the path with the usual file discovery. This means that it will import library.ts, but it would also find definition files like library.d.ts or import files in a library/ folder.
That last case might be desirable if you're joining those files together into a library.js output, but to do that you're going to be looking at either lots of nested tsconfig.json files (messy) or possibly the pre-transpiled output of another library.
The compiler takes a module kind flag:
--module ES2015
And you'll also need to be targeting ECMAScript 6 / 2015...
--target ES2015
You need both the module kind and the compilation target to be ECMAScript 2015 minimum to have "zero transformation imports".
Your import statements should look half-way between your two examples:
import {myFunction} from './library';
Additional Notes
There is still clearly a lot of discussion about module resolution... there is the TC39 specification, and the WHATWG specification - plus Node is currently still file-extention-less... looks like RequireJS might live longer than we all thought... please see:
The TypeScript thread for supporting file extensions during import transpilation (i.e. will it add the file extension?).
Recommendation
Stick with a module loader, for example RequireJS or SystemJS. This also means your modules can be shared between browser and server by using UMD or System module kinds repectively.
Obviously, once the ECMAScript discussion reaches a conclusion this will need a revisit.
For a personal project I went the other way. Since I had NPM calling a shell script to copy index.html over to the /build folder, I had the shell script then mass-rename all .js files to have no extension at all.
I did have to inform IIS in "MIME Types" section that an extension-less file should have MIME type application/javascript for that particular site, but it did indeed work. No webpack, no SystemJS, nothing. Index.html just had a hard-coded
<script type="module">
import "./app";
</script>
This was important because I was using a testing framework jest which did not like me putting the .js into the typescript import statements.

How to import static url using webpack

How can I import a static url using webpack:
index.js
import 'http://google.com/myscript.js'
It's really unclear what you're trying to do, but in general you have a few options.
Pre-download the script or install it via NPM. This probably is the preferred way to deal with external dependencies. Once it is local you can easily import or require it like any other module.
If it absolutely must be loaded dynamically you will need a 3rd party module such as https://www.npmjs.com/package/scriptjs which can easily download 3rd party modules at runtime and block the execution of the rest of the script until it has been parsed.
Use a <script> tag and include it on your page. This only works if it's a general dependency that can be loaded before everything else (maybe for a polyfill or a library you depend on everywhere like jquery.)
I hope that helps!
This webpack issue says you can use this comment to allow the import to just work. Though this is only dynamic import not static.
import(/* webpackIgnore: true */ "https://example.com");
First seen here https://stackoverflow.com/a/69951351/4619267
import is es6. With es5 and webpack, use require, or better wrap your JS files with AMD/UMD.

Categories