Skip to content

Commit

Permalink
Use a script provider to only load scripts once
Browse files Browse the repository at this point in the history
  • Loading branch information
alexpozzi committed Jul 20, 2021
1 parent dc6a3e1 commit 3148be8
Show file tree
Hide file tree
Showing 13 changed files with 139 additions and 45 deletions.
3 changes: 1 addition & 2 deletions fixtures/scripts/postRender.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@

postRenderFunction = () => {
script = () => {
if ('/post-render-script.html' === window.location.pathname) {
const paragraphElement = document.createElement('p')
const textNode = document.createTextNode("This element has been created by the post render script.");
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "chromium-pool",
"name": "server-side-renderer",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
Expand Down
4 changes: 2 additions & 2 deletions scripts/postRender.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
* In order to use a custom post render script you have to mount your own
* script as a Docker volume to `/app/scripts/postRender.js` location.
* Your custom script will just have to assign the function you want to execute
* to the `postRenderFunction` variable.
* to the `script` variable.
*
* Example:
* ```javascript
* // Adds the serialized Redux store
* postRenderFunction = function () {
* script = function () {
* var preloadedState = yourReduxStore.getState();
* var script = document.createElement('script');
* script.type = 'text/javascript';
Expand Down
7 changes: 7 additions & 0 deletions src/__snapshots__/configuration.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`configuration throws an exception when the log configuration is invalid 1`] = `"Invalid configuration."`;

exports[`configuration throws an exception when the queue configuration is invalid 1`] = `"Invalid configuration."`;

exports[`configuration throws an exception when the worker configuration is invalid 1`] = `"Invalid configuration."`;
6 changes: 3 additions & 3 deletions src/configuration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,17 +225,17 @@ describe('configuration', () => {
process.env.QUEUE_REDIS_DSN = 'redis://redis:6379'
process.env.LOG_LEVEL = 'invalid'

expect(() => createConfiguration()).toThrow('Invalid configuration.')
expect(() => createConfiguration()).toThrowErrorMatchingSnapshot()
})

it(`throws an exception when the queue configuration is invalid`, () => {
expect(() => createConfiguration()).toThrow('Invalid configuration.')
expect(() => createConfiguration()).toThrowErrorMatchingSnapshot()
})

it(`throws an exception when the worker configuration is invalid`, () => {
process.env.QUEUE_REDIS_DSN = 'redis://redis:6379'
process.env.WORKER_RENDERER_REDIRECTIONS = 'http://example.com|'

expect(() => createConfiguration()).toThrow('Invalid configuration.')
expect(() => createConfiguration()).toThrowErrorMatchingSnapshot()
})
})
5 changes: 5 additions & 0 deletions src/worker/__snapshots__/scriptProvider.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`worker :: scriptProvider throws an exception when requesting a non supported script 1`] = `"Non supported script key. \\"nonSupportedScript\\" provided but expected one of postRender."`;

exports[`worker :: scriptProvider throws an exception when the script content is invalid 1`] = `"The script provided doesn't set the script variable as a valid function."`;
6 changes: 5 additions & 1 deletion src/worker/initWorker.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { call, pipe, tap } from 'ramda'
import createScriptProvider from './scriptProvider'
import queueProcessHandler from './queue/processHandler'

// initWorker :: (Configuration, Logger, Queue) -> Function
export default (configuration, logger, queue) => call(pipe(
tap(() => logger.debug('Initializing worker.')),
tap(() => queue.process(1, queueProcessHandler(configuration, logger))),
tap(pipe(
() => createScriptProvider(),
scriptProvider => queue.process(1, queueProcessHandler(configuration, logger, scriptProvider)),
)),
tap(() => logger.debug('Worker initialized.')),
// Returns a function to be used to gracefully shutdown the worker
() => async () => {
Expand Down
6 changes: 3 additions & 3 deletions src/worker/queue/processHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import render from '../renderers/chrome'
const isJobExpired = (configuration, job) =>
Date.now() - job.data.queuedAt > configuration.queue.job.stale_timeout

// queueProcessHandler :: (Configuration, Logger) -> Function
export default (configuration, logger) => async job => {
// queueProcessHandler :: (Configuration, Logger, ScriptProvider) -> Function
export default (configuration, logger, scriptProvider) => async job => {
if (isJobExpired(configuration, job)) {
throw new Error(`Job "${job.id}" with url "${job.data.url}" timed out."`)
}

logger.debug(`Processing job "${job.id}" with url "${job.data.url}".`)

return await render(configuration, logger)(job.data.url)
return await render(configuration, logger, scriptProvider)(job.data.url)
}
4 changes: 2 additions & 2 deletions src/worker/queue/processHandler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ describe('worker :: queue :: processHandler', () => {
},
}

expect(processHandler(configuration, loggerMock)(jobMock)) // eslint-disable-line jest/valid-expect
expect(processHandler(configuration, loggerMock, {})(jobMock)) // eslint-disable-line jest/valid-expect
.resolves.toBe('My content')
expect(render).toHaveBeenCalledTimes(1)
expect(render).toHaveBeenCalledWith(configuration, loggerMock)
expect(render).toHaveBeenCalledWith(configuration, loggerMock, {})
expect(rendererMock).toHaveBeenCalledTimes(1)
expect(rendererMock).toHaveBeenCalledWith('http://nginx/dynamic.html')
})
Expand Down
37 changes: 7 additions & 30 deletions src/worker/renderers/chrome/renderer.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,11 @@
import { POST_RENDER_SCRIPT_KEY } from '../../scriptProvider'
import browserRequestHandler from './browserRequestHandler'
import { formatException } from './../../../logger'
import fs from 'fs'
import getBrowserProvider from './browserProvider'
import path from 'path'
import { reduce } from 'ramda'
import vm from 'vm'

const POST_RENDER_SCRIPT_PATH = path.join(process.cwd(), 'scripts/postRender.js')

const resolvePostRenderFunction = () => {
const context = { postRenderFunction: () => {} }

const fileStats = fs.statSync(POST_RENDER_SCRIPT_PATH, { throwIfNoEntry: false })
if (fileStats && fileStats.isFile()) {
const script = new vm.Script(
fs.readFileSync(
POST_RENDER_SCRIPT_PATH,
{ encoding: 'utf8', flag: 'r' },
),
)

vm.createContext(context)
script.runInContext(context)
}

return context.postRenderFunction
}

// renderPageContent :: (Configuration, Logger, BrowserInstance, String) -> RenderedPage
const renderPageContent = async (configuration, logger, browserInstance, url) => {
// renderPageContent :: (Configuration, Logger, ScriptProvier, BrowserInstance, String) -> RenderedPage
const renderPageContent = async (configuration, logger, scriptProvider, browserInstance, url) => {
const page = await browserInstance.newPage()

await page.setRequestInterception(true)
Expand Down Expand Up @@ -58,18 +35,18 @@ const renderPageContent = async (configuration, logger, browserInstance, url) =>
timeout: configuration.worker.renderer.timeout,
})

await page.evaluate(resolvePostRenderFunction())
await page.evaluate(scriptProvider.get(POST_RENDER_SCRIPT_KEY))

return await page.content()
}

// render :: (Configuration, Logger) -> String
export default (configuration, logger) => async url => {
// render :: (Configuration, Logger, ScriptProvider) -> String
export default (configuration, logger, scriptProvider) => async url => {
const browserProvider = getBrowserProvider(configuration, logger)
const browserInstance = await browserProvider.getInstance()

try {
return await renderPageContent(configuration, logger, browserInstance, url)
return await renderPageContent(configuration, logger, scriptProvider, browserInstance, url)
} catch (error) {
logger.error(
`An error occurred while rendering the url "${url}".`,
Expand Down
10 changes: 9 additions & 1 deletion src/worker/renderers/chrome/renderer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ describe('worker :: renderer', () => {
},
}

const postRenderScriptMock = () => {}
const scriptProviderMock = {
get: jest.fn(() => postRenderScriptMock),
}

const browserCleanupMock = jest.fn()

const pageMock = {
Expand All @@ -45,7 +50,9 @@ describe('worker :: renderer', () => {
cleanup: browserCleanupMock,
}))

expect(await render(configuration, {})('https://nginx/dynamic.html')).toBe('My page content')
expect(await render(configuration, {}, scriptProviderMock)('https://nginx/dynamic.html')).toBe('My page content')
expect(scriptProviderMock.get).toHaveBeenCalledTimes(1)
expect(scriptProviderMock.get).toHaveBeenNthCalledWith(1, 'postRender')
expect(pageMock.setRequestInterception).toHaveBeenCalledTimes(1)
expect(pageMock.setRequestInterception).toHaveBeenCalledWith(true)
expect(pageMock.on).toHaveBeenCalledTimes(5)
Expand All @@ -60,6 +67,7 @@ describe('worker :: renderer', () => {
timeout: 20000,
})
expect(pageMock.evaluate).toHaveBeenCalledTimes(1)
expect(pageMock.evaluate).toHaveBeenNthCalledWith(1, postRenderScriptMock)
expect(browserCleanupMock).toHaveBeenCalledTimes(1)
})

Expand Down
62 changes: 62 additions & 0 deletions src/worker/scriptProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { __, includes, pipe } from 'ramda'
import fs from 'fs'
import path from 'path'
import vm from 'vm'

/**
* @type ScriptProvider = {
* get :: String -> Function,
* }
*/

export const POST_RENDER_SCRIPT_KEY = 'postRender'
const VALID_SCRIPT_KEYS = [POST_RENDER_SCRIPT_KEY]

// isScriptKeyValid :: String -> Boolean
const isScriptKeyValid = includes(__, VALID_SCRIPT_KEYS)

// resolveScriptPath :: String -> String
const resolveScriptPath = key => path.join(process.cwd(), `scripts/${key}.js`)

// resolveScriptContent :: String -> String
const resolveScriptContent = pipe(
resolveScriptPath,
scriptPath => fs.readFileSync(scriptPath, { encoding: 'utf8', flag: 'r' }),
)

// resolveScript :: String -> Function
const resolveScript = pipe(
resolveScriptContent,
scriptContent => {
const script = new vm.Script(scriptContent)

const context = { script: null }
vm.createContext(context)

script.runInContext(context)

if (typeof context.script !== 'function') {
throw new Error(`The script provided doesn't set the script variable as a valid function.`)
}

return context.script
},
)

// createScriptProvider :: () -> ScriptProvider
export default () => ({
_scripts: {},

get: function (key) {
if (!isScriptKeyValid(key)) {
throw new Error(`Non supported script key. "${key}" provided but expected one of ${VALID_SCRIPT_KEYS.join(', ')}.`)
}

// eslint-disable-next-line no-prototype-builtins
if (!this._scripts.hasOwnProperty(key)) {
this._scripts[key] = resolveScript(key)
}

return this._scripts[key]
},
})
32 changes: 32 additions & 0 deletions src/worker/scriptProvider.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import createScriptProvider from './scriptProvider'
import fs from 'fs'

jest.mock('fs')

beforeEach(() => {
fs.readFileSync.mockClear()
})

describe('worker :: scriptProvider', () => {
it(`throws an exception when requesting a non supported script`, () => {
const scriptProvider = createScriptProvider()

expect(() => scriptProvider.get('nonSupportedScript')).toThrowErrorMatchingSnapshot()
})

it(`throws an exception when the script content is invalid`, () => {
const scriptProvider = createScriptProvider()

fs.readFileSync.mockReturnValueOnce('notTheRightVariable = () => {};')

expect(() => scriptProvider.get('postRender')).toThrowErrorMatchingSnapshot()
})

it(`returns the postRender script`, () => {
const scriptProvider = createScriptProvider()

fs.readFileSync.mockReturnValueOnce('script = () => {};')

expect(scriptProvider.get('postRender')).toEqual(expect.any(Function))
})
})

0 comments on commit 3148be8

Please sign in to comment.