diff --git a/src/__tests__/app.node.js b/src/__tests__/app.node.js
index 0f31dbf9..9880dbd6 100644
--- a/src/__tests__/app.node.js
+++ b/src/__tests__/app.node.js
@@ -10,8 +10,6 @@ import test from 'tape-cup';
import App from '../index';
import {compose} from '../compose.js';
-import type {Context} from '../types.js';
-
test('context composition', async t => {
const element = 'hello';
const render = el => `
${el} `;
@@ -19,17 +17,10 @@ test('context composition', async t => {
ctx.element = ctx.element.toUpperCase();
return next();
};
- const chunkUrlMap = new Map();
- const chunkIdZero = new Map();
- chunkIdZero.set('es5', 'es5-file.js');
- chunkUrlMap.set(0, chunkIdZero);
+
const context = {
headers: {accept: 'text/html'},
path: '/',
- syncChunks: [0],
- preloadChunks: [],
- chunkUrlMap,
- webpackPublicPath: '/',
element: null,
rendered: null,
render: null,
@@ -53,42 +44,3 @@ test('context composition', async t => {
}
t.end();
});
-
-test('context composition with a cdn', async t => {
- const element = 'hello';
- const render = el => `${el} `;
- const wrap = () => (ctx: Context, next: () => Promise) => {
- ctx.element = ctx.element.toUpperCase();
- return next();
- };
- const chunkUrlMap = new Map();
- const chunkIdZero = new Map();
- chunkIdZero.set('es5', 'es5-file.js');
- chunkUrlMap.set(0, chunkIdZero);
- const context = {
- headers: {accept: 'text/html'},
- path: '/',
- syncChunks: [0],
- preloadChunks: [],
- chunkUrlMap,
- webpackPublicPath: 'https://something.com/lol',
- element: null,
- rendered: null,
- render: null,
- type: null,
- body: null,
- };
-
- const app = new App(element, render);
- app.middleware(wrap());
- app.resolve();
- const middleware = compose(app.plugins);
- try {
- await middleware(((context: any): Context), () => Promise.resolve());
- // $FlowFixMe
- t.ok(context.body.includes('https://something.com/lol/es5-file.js'));
- } catch (e) {
- t.ifError(e, 'something went wrong');
- }
- t.end();
-});
diff --git a/src/__tests__/env.node.js b/src/__tests__/env.node.js
index 248e9805..20055548 100644
--- a/src/__tests__/env.node.js
+++ b/src/__tests__/env.node.js
@@ -12,58 +12,34 @@ import {loadEnv} from '../get-env.js';
tape('loadEnv defaults', t => {
const env = loadEnv()();
t.deepEqual(env, {
- rootDir: '.',
- env: 'development',
- prefix: '',
- assetPath: '/_static',
- baseAssetPath: '/_static',
- cdnUrl: '',
- webpackPublicPath: '/_static',
+ prefix: void 0,
+ cdnUrl: void 0,
});
t.end();
});
tape('loadEnv overrides', t => {
- process.env.ROOT_DIR = 'test_root_dir';
- process.env.NODE_ENV = 'production';
- process.env.ROUTE_PREFIX = 'test_route_prefix';
- process.env.FRAMEWORK_STATIC_ASSET_PATH = '/test_framework';
- process.env.CDN_URL = 'test_cdn_url';
+ process.env.ROUTE_PREFIX = '/test_route_prefix';
+ process.env.CDN_URL = 'https://cdn.com';
const env = loadEnv()();
t.deepEqual(env, {
- rootDir: 'test_root_dir',
- env: 'production',
- prefix: 'test_route_prefix',
- assetPath: 'test_route_prefix/test_framework',
- baseAssetPath: '/test_framework',
- cdnUrl: 'test_cdn_url',
- webpackPublicPath: 'test_cdn_url',
+ prefix: '/test_route_prefix',
+ cdnUrl: 'https://cdn.com',
});
- process.env.ROOT_DIR = '';
- process.env.NODE_ENV = '';
- process.env.ROUTE_PREFIX = '';
- process.env.FRAMEWORK_STATIC_ASSET_PATH = '';
- process.env.CDN_URL = '';
+ delete process.env.ROUTE_PREFIX;
+ delete process.env.CDN_URL;
t.end();
});
tape('loadEnv validation', t => {
- process.env.NODE_ENV = 'LOL';
- t.throws(loadEnv, /Invalid NODE_ENV loaded/);
- process.env.NODE_ENV = '';
-
process.env.ROUTE_PREFIX = 'test/';
t.throws(loadEnv, /ROUTE_PREFIX must not end with /);
- process.env.ROUTE_PREFIX = '';
-
- process.env.FRAMEWORK_STATIC_ASSET_PATH = 'test/';
- t.throws(loadEnv, /FRAMEWORK_STATIC_ASSET_PATH must not end with /);
- process.env.FRAMEWORK_STATIC_ASSET_PATH = '';
+ delete process.env.ROUTE_PREFIX;
- process.env.CDN_URL = 'test/';
+ process.env.CDN_URL = 'https://cdn.com/test/';
t.throws(loadEnv, /CDN_URL must not end with /);
- process.env.CDN_URL = '';
+ delete process.env.CDN_URL;
t.end();
});
diff --git a/src/__tests__/index.node.js b/src/__tests__/index.node.js
index 39ce5ef6..ca491f69 100644
--- a/src/__tests__/index.node.js
+++ b/src/__tests__/index.node.js
@@ -7,9 +7,9 @@
*/
import test from 'tape-cup';
-import App, {html} from '../index';
+import App from '../index';
import {run} from './test-helper';
-import {SSRDeciderToken} from '../tokens';
+import {SSRDeciderToken, SSRBodyTemplateToken} from '../tokens';
import {createPlugin} from '../create-plugin';
import BaseApp from '../base-app';
@@ -67,8 +67,8 @@ test('ssr with accept header', async t => {
// $FlowFixMe
const ctx = await run(app);
t.equals(typeof ctx.rendered, 'string', 'ctx.rendered');
- t.equals(typeof ctx.body, 'string', 'renders ctx.body to string');
- t.ok(!ctx.body.includes(element), 'does not renders element into ctx.body');
+ // t.equals(typeof ctx.body, 'string', 'renders ctx.body to string');
+ // t.ok(!ctx.body.includes(element), 'does not renders element into ctx.body');
t.ok(flags.render, 'calls render');
} catch (e) {
t.ifError(e, 'should not error');
@@ -203,17 +203,7 @@ test('disable SSR by composing SSRDecider with a function', async t => {
t.end();
});
-test('SSR extension handling', async t => {
- const extensionToSSRSupported = {
- js: false,
- gif: false,
- jpg: false,
- png: false,
- pdf: false,
- json: false,
- html: true,
- };
-
+test('no SSR for asset paths', async t => {
const flags = {render: false};
const element = 'hi';
const render = () => {
@@ -226,20 +216,13 @@ test('SSR extension handling', async t => {
}
try {
- for (let i in extensionToSSRSupported) {
- flags.render = false;
- let initialCtx = {
- path: `/some-path.${i}`,
- };
- // $FlowFixMe
- await run(buildApp(), initialCtx);
- const shouldSSR = extensionToSSRSupported[i];
- t.equals(
- flags.render,
- shouldSSR,
- `extension of ${i} should ${shouldSSR ? '' : 'not'} have ssr`
- );
- }
+ flags.render = false;
+ let initialCtx = {
+ path: `/_static/foo`,
+ };
+ // $FlowFixMe
+ await run(buildApp(), initialCtx);
+ t.equals(flags.render, false, `request to static asset dir should not ssr`);
} catch (e) {
t.ifError(e, 'does not error');
}
@@ -374,94 +357,27 @@ test('SSR with redirects upstream', async t => {
t.end();
});
-test('HTML escaping works', async t => {
+test('SSRBodyTemplate is used', async t => {
const element = 'hi';
const render = el => el;
- const template = (ctx, next) => {
- ctx.template.htmlAttrs = {lang: '">'};
- // $FlowFixMe
- ctx.template.bodyAttrs = {test: '">'};
- ctx.template.title = '';
- return next();
- };
const app = new App(element, render);
- app.middleware(template);
-
- try {
- // $FlowFixMe
- const ctx = await run(app);
- t.ok(ctx.body.includes(''), 'lang works');
- t.ok(ctx.body.includes(''), 'bodyAttrs work');
- t.ok(
- ctx.body.includes('\\u003C\\u002Ftitle\\u003E '),
- 'title works'
- );
- } catch (e) {
- t.ifError(e, 'does not error');
- }
- t.end();
-});
+ let called = false;
+ app.register(SSRBodyTemplateToken, ctx => {
+ called = true;
+ return `${ctx.rendered}`;
+ });
-test('head and body must be sanitized', async t => {
- const element = 'hi';
- const render = el => el;
- const template = (ctx, next) => {
- ctx.template.head.push(html` '}" />`);
- ctx.template.body.push(html`${'">'}
`);
- return next();
- };
- const app = new App(element, render);
- app.middleware(template);
try {
// $FlowFixMe
const ctx = await run(app);
- t.ok(ctx.body.includes(' '), 'head works');
- t.ok(ctx.body.includes('\\u0022\\u003E
'), 'body works');
+ t.equal(ctx.body, `hi`);
+ t.ok(called, 'ssrBodyTemplate called');
} catch (e) {
t.ifError(e, 'does not error');
}
t.end();
});
-test('throws if head is not sanitized', async t => {
- const element = 'hi';
- const render = el => el;
- const template = (ctx, next) => {
- // $FlowFixMe
- ctx.template.head.push(` '}" />`);
- return next();
- };
- const app = new App(element, render);
- app.middleware(template);
- try {
- await run(app);
- t.fail('should throw');
- } catch (e) {
- t.ok(e, 'throws if head is not sanitized');
- }
- t.end();
-});
-
-test('throws if body is not sanitized', async t => {
- const element = 'hi';
- const render = el => el;
- const template = (ctx, next) => {
- // $FlowFixMe
- ctx.template.body.push(` '}" />`);
- return next();
- };
- const app = new App(element, render);
- app.middleware(template);
-
- try {
- await run(app);
- t.fail('should throw');
- } catch (e) {
- t.ok(e, 'throws if body is not sanitized');
- }
- t.end();
-});
-
test('rendering error handling', async t => {
const element = 'hi';
const render = () => {
diff --git a/src/base-app.js b/src/base-app.js
index 95de201f..4f5f1fe3 100644
--- a/src/base-app.js
+++ b/src/base-app.js
@@ -8,15 +8,10 @@
import {createPlugin} from './create-plugin';
import {createToken, TokenType, TokenImpl} from './create-token';
-import {
- ElementToken,
- RenderToken,
- SSRDeciderToken,
- SSRBodyTemplateToken,
-} from './tokens';
-import {SSRDecider, SSRBodyTemplate} from './plugins/ssr';
+import {ElementToken, RenderToken, SSRDeciderToken} from './tokens';
import type {aliaser, cleanupFn, FusionPlugin, Token} from './types.js';
+import {SSRDecider} from './plugins/ssr';
class FusionApp {
constructor(el: Element | string, render: *) {
@@ -27,7 +22,6 @@ class FusionApp {
el && this.register(ElementToken, el);
render && this.register(RenderToken, render);
this.register(SSRDeciderToken, SSRDecider);
- this.register(SSRBodyTemplateToken, SSRBodyTemplate);
}
// eslint-disable-next-line
diff --git a/src/flow/flow-fixtures.js b/src/flow/flow-fixtures.js
index 9b4a55c1..483dde99 100644
--- a/src/flow/flow-fixtures.js
+++ b/src/flow/flow-fixtures.js
@@ -127,23 +127,10 @@ async function cleanup() {
/* - Case: getEnv typing */
async function checkEnv() {
- const {
- rootDir,
- env,
- prefix,
- assetPath,
- baseAssetPath,
- cdnUrl,
- webpackPublicPath,
- } = getEnv();
+ const {prefix, cdnUrl} = getEnv();
return {
- rootDir,
- env,
prefix,
- assetPath,
- baseAssetPath,
cdnUrl,
- webpackPublicPath,
};
}
diff --git a/src/get-env.js b/src/get-env.js
index 459e8162..f4d3859a 100644
--- a/src/get-env.js
+++ b/src/get-env.js
@@ -7,51 +7,42 @@
*/
/* eslint-env node */
import assert from 'assert';
+import {URL} from 'url';
export default (__BROWSER__ ? () => {} : loadEnv());
-function load(key, value) {
- return process.env[key] || value;
+function load(key) {
+ const value = process.env[key];
+ if (value === null) {
+ return void 0;
+ }
+ return value;
}
export function loadEnv() {
- const rootDir = load('ROOT_DIR', '.');
- const env = load('NODE_ENV', 'development');
- if (!(env === 'development' || env === 'production' || env === 'test')) {
- throw new Error(`Invalid NODE_ENV loaded: ${env}.`);
+ let prefix = load('ROUTE_PREFIX');
+ if (typeof prefix === 'string') {
+ assert(!prefix.endsWith('/'), 'ROUTE_PREFIX must not end with /');
+ assert(prefix.startsWith('/'), 'ROUTE_PREFIX must start with /');
+ }
+
+ let cdnUrl = load('CDN_URL');
+ if (typeof cdnUrl === 'string') {
+ assert(!cdnUrl.endsWith('/'), 'CDN_URL must not end with /');
+ assert(new URL(cdnUrl), 'CDN_URL must be valid absolute URL');
}
- const prefix = load('ROUTE_PREFIX', '');
- assert(!prefix.endsWith('/'), 'ROUTE_PREFIX must not end with /');
- const baseAssetPath = load('FRAMEWORK_STATIC_ASSET_PATH', `/_static`);
- assert(
- !baseAssetPath.endsWith('/'),
- 'FRAMEWORK_STATIC_ASSET_PATH must not end with /'
- );
- const cdnUrl = load('CDN_URL', '');
- assert(!cdnUrl.endsWith('/'), 'CDN_URL must not end with /');
- const assetPath = `${prefix}${baseAssetPath}`;
return function loadEnv(): Env {
return {
- rootDir,
- env,
prefix,
- assetPath,
- baseAssetPath,
cdnUrl,
- webpackPublicPath: cdnUrl || assetPath,
};
};
}
// Handle flow-types for export so browser export is ignored.
type Env = {
- rootDir: string,
- env: string,
- prefix: string,
- assetPath: string,
- baseAssetPath: string,
- cdnUrl: string,
- webpackPublicPath: string,
+ prefix?: string,
+ cdnUrl?: string,
};
declare export default () => Env;
diff --git a/src/plugins/server-context.js b/src/plugins/server-context.js
index 5019ec5a..bcdbb2d9 100644
--- a/src/plugins/server-context.js
+++ b/src/plugins/server-context.js
@@ -15,24 +15,18 @@ import type {Context} from '../types.js';
const envVars = getEnv();
export default function middleware(ctx: Context, next: () => Promise) {
- // env vars
- ctx.rootDir = envVars.rootDir;
- ctx.env = envVars.env;
- ctx.prefix = envVars.prefix;
- ctx.assetPath = envVars.assetPath;
- ctx.cdnUrl = envVars.cdnUrl;
-
- // webpack-related things
- ctx.preloadChunks = [];
- ctx.webpackPublicPath =
- ctx.webpackPublicPath || envVars.cdnUrl || envVars.assetPath;
+ let assetBase = '/_static/';
+ if (envVars.cdnUrl) {
+ assetBase = envVars.cdnUrl;
+ } else if (envVars.prefix) {
+ assetBase = envVars.prefix + assetBase;
+ }
- // these are set by fusion-cli, however since fusion-cli plugins are not added when
- // running simulation tests, it is good to default them here
- ctx.syncChunks = ctx.syncChunks || [];
- ctx.chunkUrlMap = ctx.chunkUrlMap || new Map();
-
- // fusion-specific things
+ ctx.prefix = envVars.prefix;
+ ctx.assetBase = assetBase;
+ ctx.assets = new Set();
+ ctx.criticalAssets = new Set();
+ ctx.chunkAssetIndex = new Map();
ctx.nonce = uuidv4();
ctx.useragent = new UAParser(ctx.headers['user-agent']).getResult();
ctx.element = null;
diff --git a/src/plugins/ssr.js b/src/plugins/ssr.js
index 57414a8a..99a0f828 100644
--- a/src/plugins/ssr.js
+++ b/src/plugins/ssr.js
@@ -7,7 +7,6 @@
*/
import {createPlugin} from '../create-plugin';
-import {escape, consumeSanitizedHTML} from '../sanitization';
import type {
Context,
SSRDecider as SSRDeciderService,
@@ -17,9 +16,7 @@ import type {
const SSRDecider = createPlugin({
provides: () => {
return ctx => {
- // If the request has one of these extensions, we assume it's not something that requires server-side rendering of virtual dom
- // TODO(#46): this check should probably look at the asset manifest to ensure asset 404s are handled correctly
- if (ctx.path.match(/\.(js|gif|jpg|png|pdf|json)$/)) return false;
+ if (ctx.path.startsWith(ctx.assetBase)) return false;
// The Accept header is a good proxy for whether SSR should happen
// Requesting an HTML page via the browser url bar generates a request with `text/html` in its Accept headers
// XHR/fetch requests do not have `text/html` in the Accept headers
@@ -31,54 +28,6 @@ const SSRDecider = createPlugin({
});
export {SSRDecider};
-const SSRBodyTemplate = createPlugin({
- provides: () => {
- return ctx => {
- const {htmlAttrs, bodyAttrs, title, head, body} = ctx.template;
- const safeAttrs = Object.keys(htmlAttrs)
- .map(attrKey => {
- return ` ${escape(attrKey)}="${escape(htmlAttrs[attrKey])}"`;
- })
- .join('');
-
- const safeBodyAttrs = Object.keys(bodyAttrs)
- .map(attrKey => {
- return ` ${escape(attrKey)}="${escape(bodyAttrs[attrKey])}"`;
- })
- .join('');
-
- const safeTitle = escape(title);
- // $FlowFixMe
- const safeHead = head.map(consumeSanitizedHTML).join('');
- // $FlowFixMe
- const safeBody = body.map(consumeSanitizedHTML).join('');
-
- const preloadHintLinks = getPreloadHintLinks(ctx);
- const coreGlobals = getCoreGlobals(ctx);
- const chunkScripts = getChunkScripts(ctx);
- const bundleSplittingBootstrap = [
- preloadHintLinks,
- coreGlobals,
- chunkScripts,
- ].join('');
-
- return [
- '',
- ``,
- ``,
- ` `,
- `${safeTitle} `,
- `${bundleSplittingBootstrap}${safeHead}`,
- ``,
- `${ctx.rendered}${safeBody}`,
- '',
- ].join('');
- };
- },
-});
-
-export {SSRBodyTemplate};
-
export default function createSSRPlugin({
element,
ssrDecider,
@@ -86,7 +35,7 @@ export default function createSSRPlugin({
}: {
element: any,
ssrDecider: SSRDeciderService,
- ssrBodyTemplate: SSRBodyTemplateService,
+ ssrBodyTemplate?: SSRBodyTemplateService,
}) {
return async function ssrPlugin(ctx: Context, next: () => Promise) {
if (!ssrDecider(ctx)) return next();
@@ -111,60 +60,8 @@ export default function createSSRPlugin({
return;
}
- ctx.body = ssrBodyTemplate(ctx);
- };
-}
-
-function getCoreGlobals(ctx) {
- const {webpackPublicPath, nonce} = ctx;
-
- return [
- ``,
- ].join('');
-}
-
-function getUrls({chunkUrlMap, webpackPublicPath}, chunks) {
- return [...new Set(chunks)].map(id => {
- let url = chunkUrlMap.get(id).get('es5');
- if (webpackPublicPath.endsWith('/')) {
- url = webpackPublicPath + url;
- } else {
- url = webpackPublicPath + '/' + url;
+ if (ssrBodyTemplate) {
+ ctx.body = ssrBodyTemplate(ctx);
}
- return {id, url};
- });
-}
-
-function getChunkScripts(ctx) {
- const webpackPublicPath = ctx.webpackPublicPath || '';
- // cross origin is needed to get meaningful errors in window.onerror
- const crossOrigin = webpackPublicPath.startsWith('https://')
- ? ' crossorigin="anonymous"'
- : '';
- const sync = getUrls(ctx, ctx.syncChunks).map(({url}) => {
- return ``;
- });
- const preloaded = getUrls(
- ctx,
- ctx.preloadChunks.filter(item => !ctx.syncChunks.includes(item))
- ).map(({id, url}) => {
- return ``;
- });
- return [...preloaded, ...sync].join('');
-}
-
-function getPreloadHintLinks(ctx) {
- const chunks = [...ctx.preloadChunks, ...ctx.syncChunks];
- const hints = getUrls(ctx, chunks).map(({url}) => {
- return ` `;
- });
- return hints.join('');
+ };
}
diff --git a/src/server-app.js b/src/server-app.js
index 48dd76b2..1e26d825 100644
--- a/src/server-app.js
+++ b/src/server-app.js
@@ -33,7 +33,7 @@ export default function(): typeof BaseApp {
{
element: ElementToken,
ssrDecider: SSRDeciderToken,
- ssrBodyTemplate: SSRBodyTemplateToken,
+ ssrBodyTemplate: SSRBodyTemplateToken.optional,
},
ssrPlugin
);