Most of the API is for telling sofe how to resolve a service name to an actual file location. Because we don't want to favor any specific back-end technology, sofe tries to favor configuration over convention. As a result, there are a variety of approaches to setting up your project.
unpkg to find urls and host files
Any npm package can be coerced into being a sofe service, since unpkg.com hosts all files for all npm packages. This is automatically done by sofe whenever the package.json for an npm package does not have a valid sofe
property.
unpkg to find urls, your own CDN for files
Simply put a distributable file on a CDN, modify the package.json, and publish the service to npm -- then any application can automatically resolve and load the service.
For example, a sofe-hello-world
service could automatically be resolved if sofe-hello-world
is published to npm with the following
package.json
:
{
"name": "sofe-hello-world",
"version": "1.0.0",
"sofe": {
"url": "https://unpkg.com/[email protected]/hello.js"
}
}
Put urls to all the services into the System.config
Instead of automatically resolving services, provide a manifest of services with associated service deployable locations:
System.config({
sofe: {
manifest: {
"sofe-hello-world": "https://unpkg.com/[email protected]/hello.js"
}
}
});
Put a url to a manifest file (which, in turn points to the source files) into the System.config
.
System.config({
sofe: {
manifestUrl: 'https://cdn.canopytax.com/canopy-services.json'
}
});
Manifest file format:
{
"sofe": {
"manifest": {
"sofe-hello-world": "https://unpkg.com/[email protected]/hello.js"
}
}
}
Browser storage
In addition to automatic resolution or manifest resolution, the urls to individual sofe services can be overridden with sessionStorage and/or localStorage. This is meant for times when you want to test out new changes to a service on an application where you can't easily change the System.config
(i.e., you don't own the code to the application). An override is a sessionStorage/localStorage item whose key is sofe:service-name
and whose value is a url.
Example:
window.localStorage.setItem('sofe-hello-world', 'https://unpkg.com/[email protected]/hello.js');
// OR
window.sessionStorage.setItem('sofe-hello-world', 'https://unpkg.com/[email protected]/hello.js');
If there are multiple urls that a service could be resolved to, sofe will resolve the service url in the following order (highest precedence to lowest precedence):
- Session storage
- Local storage
- The
manifest
property inside of thesofe
attribute of theSystem.config
or manifest file - The
manifestUrl
property inside ofsofe
attribute of theSystem.config
or manifest file - The
url
property inside of thesofe
attribute of the NPM package's package.json file - The
main
file inside of the NPM package's package.json file, at thelatest
version. The files themselves are retrieved from unpkg.com.
Dependencies can be loaded relative to the location of a service:
import dep from 'service/dir/dep.js!sofe';
// If the location of service is http://localhost/service.js
// then the resolved dependency will be http://localhost/dir/dep.js
Note: You cannot put ../
into a relative service path.
Because services are loaded at run-time, they cannot be bundled inside the application. Avoid bundling by using System.import
syntax instead of import
. The problem is System.import
can be cumbersome to use for all imports. If you bundle a JSPM project with sofe import
statements, you are likely to get errors that say: Uncaught (in promise) Error: This sofe service was bundled and needs to be removed from System.register
. Removing the service from System.register will allow it to be loaded at run-time, even if the project is bundled.
For example:
// main file to be bundled
import hello from 'sofe-hello-world!sofe';
hello();
<script src="dist/app-bundle.js"></script>
<script>
System.delete(System.normalizeSync('sofe-hello-world!sofe'));
</script>
Alternatively, if your project uses webpack, use the sofe-babel-plugin which allows the use of import
in bundled projects.
Sofe's configuration API exists within a System.config
property:
System.config({
sofe: {
manifest: Object, // Map of services with their distributable urls
manifestUrl: String, // Url for a manifest of available services
registry: String // Provide a custom registry defaults to "https://unpkg.com"
}
});
Manifest file format:
{
"sofe": {
"manifest": Object // Map of services with their distrubable urls
}
}
Get manifest information for available services.
import { getAllManifests } from 'sofe';
getAllManifests().then(manifests => {
manifests === {
flat: {
// Flat map of available services and urls
},
all: {
// Tree structure of all manifests
}
}
});
Returns whether or not any services have been overriden by local or session storage. Optionally, if a service name is passed, return whether or not that individual service has been overriden.
import { isOverride } from 'sofe';
if (isOverride()) {
...
}
if (isOverride('lodash')) {
...
}
Returns a promise that will resolve with a flat object of key value pairs, where the keys are service names and the values are urls. If the manifest at the specified url has a chained manifestUrl
, then
the chained manifests will be not be merged in or returned in any way.
import { getManifest } from 'sofe';
getManifest("https://example.com/sofe-manifest.json")
.then(function(manifest) {
console.log(manifest);
})
.catch(function(ex) {
throw ex;
})
Will parse out the sofe service name when given a url string or a SystemJS load
object (which has an address
property whose value is a string url).
import { getServiceName } from 'sofe';
getServiceName("https://example.com/the-name-of-my-service")
// the-name-of-my-service
getServiceName({address: "https://example.com/service-2"})
// service-2
Sofe middleware takes inspiration from the redux middleware api. We highly recommend reading Dan Abramov's above tutorial.
Essentially the middleware allows you to hook into three separate life-cycle hooks which are executed in the following order:
preLocate
- Executed before sofe resolves a service name into a urlpostLocate
- Executed after sofe resolves a service name into a urlfetch
- Executed after everything before the module is fetched
Middleware is defined as a higher-order function with each level representing a step in the resolution life-cycle. Multiple middleware's are executed in the order in which they are defined.
Example:
import { applyMiddleware } from 'sofe';
// All hooks
const canopyEnvsMiddleware = () => (preLocateLoad, preLocateNext) => {
preLocateNext(preLocateLoad);
return (postLocateLoad, postLocateNext) => {
postLocateNext(postLocateLoad);
return ({load, systemFetch}, next) => {
next(load);
}
};
}
// You don't need to call the locate methods if your
// middleware have only one argument
const logMiddleware = () => (preLocateLoad) => {
console.log('pre locate', preLocateLoad);
return (postLocateLoad) => {
console.log('post locate', postLocateLoad);
return ({load}) => {
console.log('fetch', load);
}
};
}
// If you only care about the first life-cycle
const otherMiddleware = () => (preLocateLoad, preLocateNext) => {
preLocateNext(preLocateLoad);
}
// If you only care about the last lifecycle
const yourMiddleware = () => () => () => ({load, systemFetch}, next) => {
systemFetch(load).then(next);
}
applyMiddleware(canopyEnvsMiddleware, logMiddleware, otherMiddleware, yourMiddleware)
Middleware execution order runs each middleware at each level in order before proceeding to the next level.