Skip to content

Commit

Permalink
Add sewing-kit-koa (#436)
Browse files Browse the repository at this point in the history
  • Loading branch information
lemonmade committed Dec 10, 2018
1 parent 4b9b62a commit b36df69
Show file tree
Hide file tree
Showing 11 changed files with 657 additions and 5 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -36,6 +36,7 @@ Each package has its own `README` and documentation describing usage.
| koa-shopify-graphql-proxy | [directory](packages/koa-shopify-graphql-proxy) | [![npm version](https://badge.fury.io/js/%40shopify%2Fkoa-shopify-graphql-proxy.svg)](https://badge.fury.io/js/%40shopify%2Fkoa-shopify-graphql-proxy) |
| logger | [directory](packages/logger) | [![npm version](https://badge.fury.io/js/%40shopify%2Flogger.svg)](https://badge.fury.io/js/%40shopify%2Flogger) |
| network | [directory](packages/network) | [![npm version](https://badge.fury.io/js/%40shopify%2Fnetwork.svg)](https://badge.fury.io/js/%40shopify%2Fnetwork) |
| polyfills | [directory](packages/polyfills) | [![npm version](https://badge.fury.io/js/%40shopify%2Fpolyfills.svg)](https://badge.fury.io/js/%40shopify%2Fpolyfills) |
| react-compose | [directory](packages/react-compose) | [![npm version](https://badge.fury.io/js/%40shopify%2Freact-compose.svg)](https://badge.fury.io/js/%40shopify%2Freact-compose) |
| react-effect | [directory](packages/react-effect) | [![npm version](https://badge.fury.io/js/%40shopify%2Freact-effect.svg)](https://badge.fury.io/js/%40shopify%2Freact-effect) |
| react-form-state | [directory](packages/react-form-state) | [![npm version](https://badge.fury.io/js/%40shopify%2Freact-form-state.svg)](https://badge.fury.io/js/%40shopify%2Freact-form-state) |
Expand Down
12 changes: 12 additions & 0 deletions packages/sewing-kit-koa/CHANGELOG.md
@@ -0,0 +1,12 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- `@shopify/sewing-kit-koa` package
107 changes: 107 additions & 0 deletions packages/sewing-kit-koa/README.md
@@ -0,0 +1,107 @@
# `@shopify/sewing-kit-koa`

[![Build Status](https://travis-ci.org/Shopify/quilt.svg?branch=master)](https://travis-ci.org/Shopify/quilt)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE.md) [![npm version](https://badge.fury.io/js/%40shopify%2Fsewing-kit-koa.svg)](https://badge.fury.io/js/%40shopify%2Fsewing-kit-koa.svg)

Easily access [sewing kit](https://github.com/Shopify/sewing-kit) assets from a Koa server.

## Installation

```bash
$ yarn add @shopify/sewing-kit-koa
```

## Usage

Add the supplied `middleware` to your Koa application. This is usually done near the start of your app so that all subsequent middleware can make use of Sewing Kit-related information:

```ts
import Koa from 'koa';
import {middleware} from '@shopify/sewing-kit-koa';

const app = new Koa();
app.use(middleware());
```

In subsequent middleware, you can now reference `ctx.state.assets`, which has `style` and `script` methods for fetching asset paths asynchronously:

```ts
app.use(async ctx => {
// Both `styles` and `scripts` return a Promise for an array of objects.
// Each object has a `path` for its resolved URL, and an optional `integrity`
// field for its integrity SHA. You can pass these arrays as-is into
// the `Html` component from @shopify/react-html.
const styles = (await ctx.assets.styles()).map(({path}) => path);
const scripts = (await ctx.assets.scripts()).map(({path}) => path);

ctx.body = `You need the following assets: ${[...styles, ...scripts].join(
', ',
)}`;
});
```

By default, the styles and scripts of the main bundle will be returned to you. This is the default bundle sewing kit creates, or the one you have specifically named `main`. You can optionally pass a custom name to retrieve only the assets for that bundle (which would match to the name you gave it when using [sewing kit’s entry plugin](https://github.com/Shopify/sewing-kit/blob/master/docs/plugins/entry.md)):

```ts
// In your sewing-kit.config.ts...

module.exports = function sewingKitConfig(plugins) {
return {
plugins: [
plugins.entry({
main: __dirname + '/client',
error: __dirname + '/client/error',
}),
],
};
};
```

```ts
// In your server...

app.use(async ctx => {
const styles = (await ctx.assets.styles({name: 'error'})).map(
({path}) => path,
);
const scripts = (await ctx.assets.scripts({name: 'error'})).map(
({path}) => path,
);

ctx.body = `Error page needs the following assets: ${[
...styles,
...scripts,
].join(', ')}`;
});
```

### Options

The middleware accepts some optional parameters that you can use to customize how sewing kit-generated assets will be served:

- `assetHost`: the prefix to use for all assets. This is used primary to decide where to mount a static file server if `serveAssets` is true (see next section for details). If not provided, `assetHost` will default to sewing kit’s default development asset server URL. If you set a [custom CDN](https://github.com/Shopify/sewing-kit/blob/master/docs/plugins/cdn.md) in your sewing kit config, you should pass that same value to this option.

- `serveAssets`: whether this middleware should also serve assets from within your application server. This can be useful when running the application locally, but attempting to replicate more of a production environment (and, therefore, would not be able to use the true production CDN). When this option is passed, `assetHost` must be passed with a path that can be safely mounted to for your server (this same path should be used as the custom CDN for sewing kit so that the paths sewing kit generates make sense). The middleware will then take over that endpoint for asset serving:

```ts
// In sewing-kit.config.ts...
// In this example, we want our application to serve assets only when we pass an
// environment variable that indicates we are performing an end-to-end test.

module.exports = function sewingKitConfig(plugins) {
const plugins = process.env.E2E ? [plugins.cdn('/e2e-assets/')] : [];

return {plugins};
};
```

```ts
// In your server...

app.use(
middleware({
serveAssets: true,
assetHost: '/e2e-assets/',
}),
);
```
43 changes: 43 additions & 0 deletions packages/sewing-kit-koa/package.json
@@ -0,0 +1,43 @@
{
"name": "@shopify/sewing-kit-koa",
"version": "0.0.0",
"license": "MIT",
"description": "Easily access Sewing Kit assets from a Koa server.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"sideEffects": false,
"scripts": {
"build": "tsc --p tsconfig.build.json",
"prepublishOnly": "yarn run build"
},
"publishConfig": {
"access": "public",
"@shopify:registry": "https://registry.npmjs.org"
},
"author": "Shopify Inc.",
"repository": {
"type": "git",
"url": "git+https://github.com/Shopify/quilt.git"
},
"bugs": {
"url": "https://github.com/shopify/quilt/issues"
},
"homepage": "https://github.com/Shopify/quilt/blob/master/packages/sewing-kit-koa/README.md",
"dependencies": {
"@types/koa-mount": "^3.0.1",
"@types/koa-static": "^4.0.0",
"app-root-path": "^2.1.0",
"fs-extra": "^7.0.1",
"koa-compose": "^4.1.0",
"koa-mount": "^4.0.0",
"koa-static": "^5.0.0"
},
"devDependencies": {
"@types/app-root-path": "^1.2.4",
"@types/fs-extra": "^5.0.4",
"typescript": "~3.0.1"
},
"files": [
"dist/*"
]
}
88 changes: 88 additions & 0 deletions packages/sewing-kit-koa/src/assets.ts
@@ -0,0 +1,88 @@
import {join} from 'path';
import {readJson} from 'fs-extra';
import appRoot from 'app-root-path';

export interface Asset {
path: string;
integrity?: string;
}

interface Entrypoint {
js: Asset[];
css: Asset[];
}

interface AssetList {
entrypoints: {[key: string]: Entrypoint};
}

interface Options {
assetHost: string;
}

export default class Assets {
assetHost: string;
private resolvedAssetList?: AssetList;

constructor({assetHost}: Options) {
this.assetHost = assetHost;
}

async scripts({name = 'main'} = {}) {
const {js} = getAssetsForEntrypoint(name, await this.getAssetList());

const scripts =
// Sewing Kit does not currently include the vendor DLL in its asset
// manifest, so we manually add it here (it only exists in dev).
// eslint-disable-next-line no-process-env
process.env.NODE_ENV === 'development'
? [{path: `${this.assetHost}dll/vendor.js`}, ...js]
: js;

return scripts;
}

async styles({name = 'main'} = {}) {
const {css} = getAssetsForEntrypoint(name, await this.getAssetList());
return css;
}

private async getAssetList() {
if (this.resolvedAssetList) {
return this.resolvedAssetList;
}

this.resolvedAssetList = await loadConsolidatedManifest();
return this.resolvedAssetList;
}
}

let consolidatedManifestPromise: Promise<AssetList> | null = null;

function loadConsolidatedManifest() {
if (consolidatedManifestPromise) {
return consolidatedManifestPromise;
}

consolidatedManifestPromise = readJson(
join(appRoot.path, 'build/client/assets.json'),
);

return consolidatedManifestPromise;
}

export function internalOnlyClearCache() {
consolidatedManifestPromise = null;
}

function getAssetsForEntrypoint(name: string, {entrypoints}: AssetList) {
if (!entrypoints.hasOwnProperty(name)) {
throw new Error(
`No entrypoints found with the name '${name}'. Available entrypoints: ${Object.keys(
entrypoints,
).join(', ')}`,
);
}

return entrypoints[name];
}
2 changes: 2 additions & 0 deletions packages/sewing-kit-koa/src/index.ts
@@ -0,0 +1,2 @@
export {default as Assets} from './assets';
export {default as middleware} from './middleware';
53 changes: 53 additions & 0 deletions packages/sewing-kit-koa/src/middleware.ts
@@ -0,0 +1,53 @@
import {join} from 'path';
import {Context} from 'koa';
import serve from 'koa-static';
import compose from 'koa-compose';
import mount from 'koa-mount';
import appRoot from 'app-root-path';

import Assets, {Asset} from './assets';

export {Assets, Asset};

export interface State {
assets: Assets;
}

export interface Options {
assetHost?: string;
serveAssets?: boolean;
}

export default function middleware({
serveAssets = false,
assetHost = defaultAssetHost(serveAssets),
}: Options = {}) {
async function sewingKitMiddleware(ctx: Context, next: () => Promise<any>) {
const assets = new Assets({
assetHost,
});
ctx.state.assets = assets;
await next();
}

return serveAssets && assetHost.startsWith('/')
? compose([
sewingKitMiddleware,
mount(assetHost, serve(join(appRoot.path, 'build/client'))),
])
: sewingKitMiddleware;
}

function defaultAssetHost(serveAssets: boolean) {
// In development, Sewing Kit defaults to running an asset server on
// http://localhost:8080/webpack/assets/. When running in `serveAssets`
// mode (the application server also serves the assets), we default to
// assuming they have set the asset endpoint to be under /assets. In order
// to use this feature, a developer would need to add the following to the
// Sewing Kit config that built the assets:
//
// {
// plugins: [plugins.cdn('/assets/')],
// }
return serveAssets ? '/assets/' : 'http://localhost:8080/webpack/assets/';
}

0 comments on commit b36df69

Please sign in to comment.