Skip to content

Commit

Permalink
Adds inlineStyle() and inline style map.
Browse files Browse the repository at this point in the history
Refs #41.

The new `inlineStyle()` function looks equivalent to `inlineStyleLegacy()` from the user's perspective, however the key differences are:
1. `inlineStyle()` is synchronous and returns an annotation telling the build pipeline to inline the style here. `inlineStyleLegacy()` asynchronously read the CSS file and returned the actual `<style />` tag. This change makes the function easier to use (no viral `async`) and gives the build system an opportunity to bundle the inlined CSS file which wasn't available before.
2. `inlineStyle()` looks up the given import path in the "inline style map" and uses the result as the path in the output annotation. This map serves to correlate the import path users pass into `inlineStyle()` with the actual on disk location of the CSS file. These may not align as the latter could contain an artifact root, have a different file name, or even be in a different package altogether. The renderer receives this map in its arguments and `inlineStyle()` looks up this map so generated annotations use the real file path of inline styles, while users pass in the import path.
  • Loading branch information
dgp1130 committed May 1, 2022
1 parent d7f49f9 commit 34cb490
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 6 deletions.
5 changes: 5 additions & 0 deletions packages/renderer/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ ts_library(
visibility = ["//tools/internal:__pkg__"],
deps = [
":entry_point",
"//packages/rules_prerender:inline_style_map",
"//common:binary",
"//common:formatters",
"@npm//@types/node",
Expand Down Expand Up @@ -46,9 +47,13 @@ ts_library(
srcs = ["renderer_test.ts"],
testonly = True,
deps = [
"//common/models:prerender_annotation",
"//common/models:prerender_resource",
"//common/testing:binary",
"//common/testing:temp_dir",
# Need a dependency on styles for entry points under test to import
# `includeStyle()`.
"//packages/rules_prerender:styles",
"@npm//@bazel/runfiles",
"@npm//@types/jasmine",
"@npm//@types/node",
Expand Down
52 changes: 52 additions & 0 deletions packages/renderer/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,25 @@ import * as yargs from 'yargs';
import { main } from 'rules_prerender/common/binary';
import { mdSpacing } from 'rules_prerender/common/formatters';
import { invoke } from 'rules_prerender/packages/renderer/entry_point';
import { setMap as setInlineStyleMap } from 'rules_prerender/packages/rules_prerender/inline_style_map';

main(async () => {
// Parse binary options and arguments.
const {
'entry-point': entryPoint,
'output-dir': outputDir,
'inline-style-import': inlineStyleImports = [],
'inline-style-path': inlineStylePaths = [],
} = yargs
.usage(mdSpacing(`
Invokes the given entry point which returns \`PrerenderResources\`
and writes each resource to the relevant location under
\`--output-dir\`. For any \`inlineStyle()\` calls used by the entry
point, the import path given is looked up in the inline style map
defined as "parallel array" of \`--inline-style-import\` and
\`--inline-style-path\`, where the former is a list of keys and the
latter is a list of values in the inline style map.
`))
.option('entry-point', {
type: 'string',
demandOption: true,
Expand All @@ -31,8 +43,31 @@ main(async () => {
at their specified paths.
`),
})
.option('inline-style-import', {
type: 'string',
array: true,
description: mdSpacing(`
A list of inline style import paths to combine with inline style
file paths. Each import path corresponds to the location a user
would import the inline style file listed at the same index in the
\`--inline-style-path\` list.
`),
})
.option('inline-style-path', {
type: 'string',
array: true,
description: mdSpacing(`
A list of inline style file paths to combine with inline style
imports. Each file path corresponds to the actual on-disk location
of the \`--inline-style-import\` at the same index.
`),
})
.argv;

// Pass through `--inline-style-import` and `--inline-style-path` flags as the
// inline style map to be looked up by `inlineStyle()` calls.
setInlineStyleMap(new Map(zip(inlineStyleImports, inlineStylePaths)));

// Invoke the provided entry point.
let resources: Awaited<ReturnType<typeof invoke>>;
try {
Expand Down Expand Up @@ -69,3 +104,20 @@ main(async () => {

return 0;
});

/**
* Given two arrays, returns an {@link Iterable} of pairs of the same index.
* Assumes the two inputs have the same number of items.
*/
function* zip<First, Second>(firsts: First[], seconds: Second[]):
Iterable<[ First, Second ]> {
if (firsts.length !== seconds.length) {
throw new Error(`Zipped arrays must be the same length, got:\n${
firsts.join(', ')}\n\n${seconds.join(', ')}`);
}
for (const [ index, first ] of firsts.entries()) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const second = seconds[index]!;
yield [ first, second ];
}
}
65 changes: 64 additions & 1 deletion packages/renderer/renderer_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,23 @@ import { promises as fs } from 'fs';
import { runfiles } from '@bazel/runfiles';
import { execBinary, ProcessResult } from 'rules_prerender/common/testing/binary';
import { useTempDir } from 'rules_prerender/common/testing/temp_dir';
import { createAnnotation, StyleScope } from 'rules_prerender/common/models/prerender_annotation';

const renderer = runfiles.resolvePackageRelative('renderer_test_binary.sh');

/** Invokes the renderer binary. */
async function run({ entryPoint, outputDir }: {
async function run({ entryPoint, outputDir, inlineStyles = new Map() }: {
entryPoint: string,
outputDir: string,
inlineStyles?: ReadonlyMap<string, string>,
}): Promise<ProcessResult> {
return await execBinary(renderer, [
'--entry-point', entryPoint,
'--output-dir', outputDir,
...Array.from(inlineStyles.entries()).flatMap(([ importPath, filePath ]) => [
'--inline-style-import', importPath,
'--inline-style-path', filePath,
]),
]);
}

Expand Down Expand Up @@ -137,6 +143,63 @@ module.exports = async function* () {
expect(world).toBe('Hello, World!');
});

it('renders mapped inline style paths', async () => {
await fs.mkdir(`${tmpDir.get()}/output`);
await fs.writeFile(`${tmpDir.get()}/foo.js`, `
// We can't rely on the linker to resolve imports for us. We also don't want to
// rely on the legacy require() patch, so instead we need to manually load the
// runfiles helper and require() the file through it.
const runfiles = require(process.env['BAZEL_NODE_RUNFILES_HELPER']);
const { PrerenderResource } = require(runfiles.resolveWorkspaceRelative('common/models/prerender_resource.js'));
const { inlineStyle } = require(runfiles.resolveWorkspaceRelative('packages/rules_prerender/styles.js'));
module.exports = async function* () {
yield PrerenderResource.of('/index.html', \`
<!DOCTYPE html>
<html>
<head>
<title>Test Page</title>
</head>
<body>
\${inlineStyle('wksp/foo/bar/baz.css')}
</body>
</html>
\`.trim());
};
`.trim());

const { code, stdout, stderr } = await run({
entryPoint: `${tmpDir.get()}/foo.js`,
outputDir: `${tmpDir.get()}/output`,
inlineStyles: new Map(Object.entries({
'wksp/foo/bar/baz.css': 'wksp/hello/world.css',
})),
});

expect(code).toBe(0, `Binary unexpectedly failed. STDERR:\n${stderr}`);
expect(stdout).toBe('');
expect(stderr).toBe('');

const index = await fs.readFile(
`${tmpDir.get()}/output/index.html`, 'utf8');
const expectedAnnotation = createAnnotation({
type: 'style',
scope: StyleScope.Inline,
path: 'wksp/hello/world.css',
});
expect(index).toBe(`
<!DOCTYPE html>
<html>
<head>
<title>Test Page</title>
</head>
<body>
<!-- ${expectedAnnotation} -->
</body>
</html>
`.trim());
});

it('fails from import errors from the entry point', async () => {
await fs.mkdir(`${tmpDir.get()}/output`);
await fs.writeFile(`${tmpDir.get()}/foo.js`, `
Expand Down
10 changes: 10 additions & 0 deletions packages/rules_prerender/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,9 @@ jasmine_node_test(
ts_library(
name = "styles",
srcs = ["styles.ts"],
visibility = ["//packages/renderer:__subpackages__"],
deps = [
":inline_style_map",
"//common:fs",
"//common/models:prerender_annotation",
"@npm//@bazel/runfiles",
Expand All @@ -184,7 +186,9 @@ ts_library(
testonly = True,
data = ["//packages/rules_prerender/testdata:styles.css"],
deps = [
":inline_style_map",
":styles",
"//common/models:prerender_annotation",
"@npm//@types/jasmine",
],
)
Expand All @@ -193,3 +197,9 @@ jasmine_node_test(
name = "styles_test",
deps = [":styles_test_lib"],
)

ts_library(
name = "inline_style_map",
srcs = ["inline_style_map.ts"],
visibility = ["//packages/renderer:__subpackages__"],
)
20 changes: 20 additions & 0 deletions packages/rules_prerender/inline_style_map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* @fileoverview A global map of inline style imports to their file locations. Inlined
* styles may live at a different file location than is visible to the user, so import
* paths may not align to the actual file location. This serves as a map of "user
* import path" -> "actual file location the CSS file lives".
*/

let map: ReadonlyMap<string, string> | undefined;

/** Returns the inline style map. */
export function getMap(): ReadonlyMap<string, string> {
if (!map) throw new Error('Inline style map not set.');
return map;
}

/** Sets the inline style map. */
export function setMap(newMap: ReadonlyMap<string, string>): void {
if (map) throw new Error('Inline style map already set, cannot set it again.');
map = newMap;
}
26 changes: 26 additions & 0 deletions packages/rules_prerender/styles.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { runfiles } from '@bazel/runfiles';
import * as fs from 'rules_prerender/common/fs';
import { createAnnotation, StyleScope } from 'rules_prerender/common/models/prerender_annotation';
import { getMap as getInlineStyleMap } from 'rules_prerender/packages/rules_prerender/inline_style_map';

/**
* Returns a prerender annotation as a string to be included in prerendered
Expand All @@ -16,6 +17,31 @@ export function includeStyle(path: string): string {
return `<!-- ${annotation} -->`;
}

/**
* Returns a prerender annotation as a string to be included in prerendered
* HTML. This is used by the prerender build process to inline the referenced
* CSS file at the annotation's location in the page.
*/
export function inlineStyle(importPath: string): string {
// Look up the import path in the inline style map to get its actual file
// path on disk.
const inlineStyleMap = getInlineStyleMap();
const filePath = inlineStyleMap.get(importPath);
if (!filePath) {
throw new Error(`Could not find "${
importPath}" in the inline style map. Available imports are:\n\n${
Array.from(inlineStyleMap.keys()).join('\n')}`);
}

// Return an annotation with the real file path.
const annotation = createAnnotation({
type: 'style',
scope: StyleScope.Inline,
path: filePath,
});
return `<!-- ${annotation} -->`;
}

/**
* Reads the given CSS file at the provided runfiles path and returns it in a
* `<style />` tag to be inlined in the document.
Expand Down
45 changes: 40 additions & 5 deletions packages/rules_prerender/styles_test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,49 @@
import 'jasmine';

import { includeStyle, inlineStyleLegacy } from 'rules_prerender/packages/rules_prerender/styles';
import * as inlineStyleMap from 'rules_prerender/packages/rules_prerender/inline_style_map';
import { createAnnotation, StyleScope } from 'rules_prerender/common/models/prerender_annotation';
import { includeStyle, inlineStyle, inlineStyleLegacy } from 'rules_prerender/packages/rules_prerender/styles';

describe('styles', () => {
describe('includeStyle()', () => {
it('returns a style annotation in an HTML comment', () => {
const annotation = includeStyle('foo/bar/baz.css');
it('returns a global style annotation in an HTML comment', () => {
const annotation = includeStyle('wksp/foo/bar/baz.css');

expect(annotation)
.toBe('<!-- bazel:rules_prerender:PRIVATE_DO_NOT_DEPEND_OR_ELSE - {"type":"style","scope":"global","path":"foo/bar/baz.css"} -->');
expect(annotation).toBe(`<!-- ${createAnnotation({
type: 'style',
scope: StyleScope.Global,
path: 'wksp/foo/bar/baz.css',
})} -->`);
});
});

describe('inlineStyle()', () => {
it('returns an inline style annotation in an HTML comment', () => {
spyOn(inlineStyleMap, 'getMap').and.returnValue(new Map(Object.entries({
'wksp/foo/bar/baz.css': 'wksp/some/real/file.css',
})));

const annotation = inlineStyle('wksp/foo/bar/baz.css');

expect(annotation).toBe(`<!-- ${createAnnotation({
type: 'style',
scope: StyleScope.Inline,
path: 'wksp/some/real/file.css',
})} -->`);
});

it('throws an error when the requested style import is not present', () => {
spyOn(inlineStyleMap, 'getMap').and.returnValue(new Map(Object.entries({
'wksp/foo/bar/baz.css': 'wksp/some/dir/baz.css',
'wksp/hello/world.css': 'wksp/goodbye/mars.css',
})));

expect(() => inlineStyle('wksp/does/not/exist.css')).toThrowError(`
Could not find "wksp/does/not/exist.css" in the inline style map. Available imports are:
wksp/foo/bar/baz.css
wksp/hello/world.css
`.trim());
});
});

Expand Down

0 comments on commit 34cb490

Please sign in to comment.