Skip to content

Commit

Permalink
feat: preload CSS in the <head /> (fixes #20)
Browse files Browse the repository at this point in the history
  • Loading branch information
gajus committed Jul 13, 2017
1 parent 9ab4e1f commit 9a9f2fe
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 14 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ export type UserConfigurationType = {
+deviceMetricsOverride?: UserDeviceMetricsOverrideType,
+extractStyles?: boolean,
+formatStyles?: FormatStylesType,
+inlineStyles?: boolean
+inlineStyles?: boolean,
+preloadStyles?: boolean
};

```
Expand All @@ -112,12 +113,13 @@ The default behaviour is to return the HTML.

|Name|Type|Description|Default value|
|---|---|---|---|
|`cookies`|`Array<{name: string, value: string}>`|Sets a cookie with the given cookie data.|N/A|
|`delay`|`number`|Defines how many milliseconds to wait after the "load" event has been fired before capturing the styles used to load the page. This is important if resources appearing on the page are being loaded asynchronously.|`number`|`5000`|
|`deviceMetricsOverride`||See [`deviceMetricsOverride` configuration](#devicemetricsoverride-configuration)||
|`cookies`|`Array<{name: string, value: string}>`|Sets a cookie with the given cookie data.|N/A|
|`extractStyles`|`boolean`|Extracts CSS used to render the page.|`false`|
|`formatStyles`|`(styles: string) => Promise<string>`|Used to format CSS. Useful with `inlineStyles=true` option to format the CSS before it is inlined.|N/A|
|`inlineStyles`|`boolean`|Inlines the styles required to render the document.|`false`|
|`formatStyles`|`(styles: string) => Promise<string>`|Used to format CSS. Useful with `--inlineStyles` option to format the CSS before it is inlined.|N/A|
|`preloadStyles`|`boolean`|Adds `rel=preload` for all styles removed from `<head>`. Used with `inlineStyles=true`.|`true`|
|`url`|`string`|The URL to render.|N/A|

#### `deviceMetricsOverride` configuration
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"chrome-remote-interface": "^0.24.1",
"death": "^1.1.0",
"debug": "^2.6.8",
"surgeon": "^2.1.0",
"yargs": "^8.0.2"
},
"description": "Webpage pre-rendering service. ⚡️",
Expand Down
5 changes: 5 additions & 0 deletions src/bin/commands/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ export const baseConfiguration = {
default: false,
description: 'Inlines the styles required to render the document.',
type: 'boolean'
},
preloadStyles: {
default: true,
description: 'Adds rel=preload for all styles removed from <head>. Used with inlineStyles=true.',
type: 'boolean'
}
};

Expand Down
4 changes: 3 additions & 1 deletion src/factories/createConfiguration.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default (userConfiguration: UserConfigurationType): ConfigurationType =>
const extractStyles = userConfiguration.extractStyles || false;
const formatStyles = userConfiguration.formatStyles;
const inlineStyles = userConfiguration.inlineStyles || false;
const preloadStyles = userConfiguration.preloadStyles !== false;

if (extractStyles && inlineStyles) {
throw new Error('inlineStyles and inlineStyles options cannot be used together.');
Expand All @@ -41,6 +42,7 @@ export default (userConfiguration: UserConfigurationType): ConfigurationType =>
deviceMetricsOverride,
extractStyles,
formatStyles,
inlineStyles
inlineStyles,
preloadStyles
};
};
6 changes: 4 additions & 2 deletions src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ export type UserConfigurationType = {
+deviceMetricsOverride?: UserDeviceMetricsOverrideType,
+extractStyles?: boolean,
+formatStyles?: FormatStylesType,
+inlineStyles?: boolean
+inlineStyles?: boolean,
+preloadStyles?: boolean
};

export type ConfigurationType = {|
Expand All @@ -42,5 +43,6 @@ export type ConfigurationType = {|
+deviceMetricsOverride: DeviceMetricsOverrideType,
+extractStyles: boolean,
+formatStyles?: FormatStylesType,
+inlineStyles: boolean
+inlineStyles: boolean,
+preloadStyles: boolean
|};
56 changes: 50 additions & 6 deletions src/usus.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from 'chrome-launcher';
import CDP from 'chrome-remote-interface';
import createDebug from 'debug';
import surgeon from 'surgeon';
import {
delay
} from 'bluefeather';
Expand All @@ -30,9 +31,11 @@ const inlineStyles = async (DOM: *, Runtime: *, rootNodeId: number, styles: stri
// e.g. How to create a new node using CDP DOM API?
await Runtime.evaluate({
expression: `
const styleElement = document.createElement('div');
styleElement.setAttribute('id', 'usus-inline-styles');
document.head.appendChild(styleElement);
{
const styleElement = document.createElement('div');
styleElement.setAttribute('id', 'usus-inline-styles');
document.head.appendChild(styleElement);
}
`
});

Expand All @@ -56,9 +59,11 @@ const inlineImports = async (DOM: *, Runtime: *, rootNodeId: number, styleImport

await Runtime.evaluate({
expression: `
const scriptElement = document.createElement('div');
scriptElement.setAttribute('id', 'usus-style-import');
document.body.appendChild(scriptElement);
{
const scriptElement = document.createElement('div');
scriptElement.setAttribute('id', 'usus-style-import');
document.body.appendChild(scriptElement);
}
`
});

Expand All @@ -75,6 +80,41 @@ const inlineImports = async (DOM: *, Runtime: *, rootNodeId: number, styleImport
});
};

const inlineStylePreload = async (DOM: *, Runtime: *, rootNodeId: number, styleImports: $ReadOnlyArray<string>) => {
// @todo See note in inlineStyles.

await Runtime.evaluate({
expression: `
{
const scriptElement = document.createElement('div');
scriptElement.setAttribute('id', 'usus-style-preload');
document.head.appendChild(scriptElement);
}
`
});

const nodeId = (await DOM.querySelector({
nodeId: rootNodeId,
selector: '#usus-style-preload'
})).nodeId;

debug('#usus-style-preload nodeId %d', nodeId);

const x = surgeon();

const styleUrls = x('select link {0,} | read attribute href', styleImports.join(''));

const stylePreloadLinks = styleUrls
.map((styleUrl) => {
return `<link rel="preload" href="${styleUrl}" as="style">`;
});

await DOM.setOuterHTML({
nodeId,
outerHTML: stylePreloadLinks.join('\n')
});
};

export const render = async (url: string, userConfiguration: UserConfigurationType = {}): Promise<string> => {
const configuration = createConfiguration(userConfiguration);

Expand Down Expand Up @@ -176,6 +216,10 @@ export const render = async (url: string, userConfiguration: UserConfigurationTy
styleImportLinks.push(styleImportNodeHtml.outerHTML);
}

if (configuration.preloadStyles) {
await inlineStylePreload(DOM, Runtime, rootDocument.root.nodeId, styleImportLinks);
}

await inlineStyles(DOM, Runtime, rootDocument.root.nodeId, usedStyles);
await inlineImports(DOM, Runtime, rootDocument.root.nodeId, styleImportLinks);

Expand Down
3 changes: 2 additions & 1 deletion test/createConfiguration.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ const createDefaultConfiguration = () => {
},
extractStyles: false,
formatStyles: undefined,
inlineStyles: false
inlineStyles: false,
preloadStyles: true
};
};

Expand Down
44 changes: 43 additions & 1 deletion test/usus.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,48 @@ test('renders HTML', async (t) => {
`));
});

test('inlines CSS', async (t) => {
test('inlines CSS (preloadStyles=false)', async (t) => {
const styleServer = await serve(`
body {
background: #f00;
}
`, 'text/css');

const server = await serve(`
<html>
<head>
<link rel='stylesheet' href='${styleServer.url}'>
</head>
<body>
<p>Hello, World!</p>
</body>
</html>
`);

const result = await render(server.url, {
delay: 500,
inlineStyles: true,
preloadStyles: false
});

await styleServer.close();
await server.close();

t.true(isHtmlEqual(result, `
<html>
<head>
<style>body { background: #f00; }</style>
</head>
<body>
<p>Hello, World!</p>
<link href="${styleServer.url}" rel="stylesheet">
</body>
</html>`
));
});

test('inlines CSS (preloadStyles=true)', async (t) => {
const styleServer = await serve(`
body {
background: #f00;
Expand Down Expand Up @@ -63,6 +104,7 @@ test('inlines CSS', async (t) => {
t.true(isHtmlEqual(result, `
<html>
<head>
<link as="style" href="${styleServer.url}" rel="preload">
<style>body { background: #f00; }</style>
</head>
<body>
Expand Down

0 comments on commit 9a9f2fe

Please sign in to comment.