Polestar
A commonjs-ish module loader for browsers, as used in Demoboard.
Polestar loads commonjs modules from NPM and/or a virtual file system on-demand. It is highly configurable, allowing you to:
- Resolve and build each
require()
call's request asynchronously (while your modules still use require()
as a synchronous function) - Set appropriate global variables for your modules, including
process
, global
, or a stubbed window
object. - Capture errors while loading modules, and forward them as appropriate.
- Display a loading indicator until the entry point is ready to execute.
yarn add polestar
Usage
import { Polestar } from 'polestar'
let polestar = new Polestar({
globals: {
process: {
env: {},
}
},
moduleThis: window,
fetcher: async (url: string, meta: FetcherMeta) =>
api.fetchModule(url, meta),
resolver: defaultResolver,
onEntry: () => {
api.dispatch('entry')
},
onError: (error) => {
console.error(error)
},
})
let indexModule = await polestar.require('vfs:///index.js')
polestar.require('npm:///some-package@latest')
polestar.require('vfs:///index.js')
polestar.require('some-loader!vfs:///index.css')
let anotherModule = await polestar.evaluate(
['react', 'react-dom'],
`
var React = require('react');
var ReactDOM = require('react-dom');
ReactDOM.render(
React.createElement('div', {}, "Hello, world!"),
document.getElementById('root')
)
`
)
Requests, URLs & Module ids
Polestar uses three different types of strings to reference modules.
Requests
Requests are the strings that appear within require()
statements. They can take any number of formats:
- Paths relative to the module that contains the
require()
call, e.g. ./App.js
- Fully qualified URLs, e.g.
https://unpkg.com/react
- Bare imports, e.g.
react
- They can be prefixed have webpack-style loader, e.g.
style-loader!css-loader!./styles.css
Whenever Polestar encounters a request, it first resolves it to either a URL, or a module id.
URLs
In polestar, a URL is a string generated by the resolver that can be passed to the fetcher to request a module's source and dependencies.
The default resolver is able to create two types of URLs:
- NPM URLs, e.g.
npm://react@latest
, are generated from bare imports. They always contain a package name and version range string (e.g. @latest
, @^1.7.2
, @16.7.0
). They can also optionally contain a path. - Other URLs are treated as relative to the module making the request.
One issue with using URLs to refer to modules is that URLs can redirect to other URLs, so a single module can be referenced by multiple different URLs. For example, each of these unpkg URLs may refer to the same module:
Because of this, polestar doesn't index modules by URL. Instead, it indexes modules by ID.
Module ids
When the fetcher returns a result for a given URL, it'll also include an ID. This ID must be a URL, and is usually the URL at the end of any redirect chain that the fetcher encounters.
Once Polestar has loaded the module, it'll notify the resolver of the ID of the new module, as well as any URLs that map to that ID. This allows the resolver to map requests to already-loaded modules where possible, speeding up load time for frequently referenced modules like react
.
A modules relative requires (e.g. require('./App.js)
) will also be resolved relative to the module ID.
Fetchers
A minimal fetcher function just takes a URL and returns the url, id, dependencies and source of the the module at that URL.
const fetcher = (url: string) => ({
url: 'vfs:///Hello.js',
id: 'vfs:///Hello.js',
dependencies: ['react']
code: `
var React = require('react')
module.exports.Hello = function() {
return React.createElement('h1', {}, 'hello')
}
`,
})
Fetcher functions also receive a meta
object with information on where the
fetch originated, which is useful for error messages.
type Fetcher = (url: string, meta: FetchMeta) => Promise<FetchResult>
interface FetchMeta {
requiredById: string,
originalRequest: string
}
UMD modules
For UMD modules, the dependencies can be omitted and replaced with the string umd
.
const fetcher = (url: string) => ({
url: 'https://unpkg.com/react@latest'
id: 'https://unpkg.com/react@16.6.3/umd/react.development.js',
dependencies: 'umd',
code: `...`,
})
This works as dependencies are already specified within the UMD module. This is especially useful for large modules like react-dom, as parsing the received code to find the requires can be quite slow.
Version Ranges
Many packages require specific versions of their dependencies to work; they won't work at all if you default to using the latest versions of all packages involved.
In order to resolve bare requests to the correct version of an NPM package, the resolver needs to have access to the dependencies
object from a module's package.json, for modules that define one. These dependencies should be returned via the dependencyVersionRanges
property of the fetcher's result.
const fetcher = (url: string) => ({
url: 'https://unpkg.com/react-dom@latest'
id: 'https://unpkg.com/react-dom@16.6.3/umd/react-dom.development.js',
code: `...`,
dependencies: 'umd',
dependencyVersionRanges: {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"scheduler": "^0.11.2"
}
})
Full Fetcher types
type VersionRanges = { [name: string]: string }
type Fetcher = (url: string, meta: FetchMeta) => Promise<FetchResult>
interface FetchMeta {
requiredById: string,
originalRequest: string
}
interface FetchResult {
id: string,
url: string,
code: string,
dependencies?: 'umd' | string[],
dependencyVersionRanges?: VersionRanges,
}