Skip to content

Latest commit

 

History

History
252 lines (207 loc) · 8.5 KB

sofe-api.md

File metadata and controls

252 lines (207 loc) · 8.5 KB

Sofe Configuration and API

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.

Approach #1

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.

Approach #2

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/sofe-hello-world@1.0.0/hello.js"
  }
}

Approach #3

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/sofe-hello-world@1.0.0/hello.js"
    }
  }
});

Approach #4

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/sofe-hello-world@1.0.0/hello.js"
    }
  }
}

Approach #5

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/sofe-hello-world@1.0.0/hello.js');
// OR
window.sessionStorage.setItem('sofe-hello-world', 'https://unpkg.com/sofe-hello-world@1.0.0/hello.js');

Resolution precedence

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):

  1. Session storage
  2. Local storage
  3. The manifest property inside of the sofe attribute of the System.config or manifest file
  4. The manifestUrl property inside of sofe attribute of the System.config or manifest file
  5. The url property inside of the sofe attribute of the NPM package's package.json file
  6. The main file inside of the NPM package's package.json file, at the latest version. The files themselves are retrieved from unpkg.com.

Relative paths

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.

When to use System.import instead of just import

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.

Full API

Configuration

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
  }
}

API

getAllManifests(): Promise

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
		}
	}
});

isOverride([service]): Boolean

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')) {
	...
}

getManifest(url): Promise

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;
})

getServiceName(string|object): String

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

Middleware

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:

  1. preLocate - Executed before sofe resolves a service name into a url
  2. postLocate - Executed after sofe resolves a service name into a url
  3. fetch - 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.