Skip to content

Commit 156c45f

Browse files
committed
Implement inlining svg -> use[xlink:href] to resolve issues related to absolute SVG paths (eg http://localhost/foo#svgId)
1 parent 64386d6 commit 156c45f

File tree

12 files changed

+164
-92
lines changed

12 files changed

+164
-92
lines changed

examples/cli/package.json

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,19 @@
1111
},
1212
"private": true,
1313
"dependencies": {
14-
"@angular/animations": "4.0.0",
14+
"@angular/animations": "4.2.2",
1515
"@angular/cli": ">=1.0.0",
16-
"@angular/common": "4.0.0",
17-
"@angular/compiler": "4.0.0",
18-
"@angular/compiler-cli": "4.0.0",
19-
"@angular/core": "4.0.0",
20-
"@angular/forms": "4.0.0",
21-
"@angular/http": "4.0.0",
16+
"@angular/common": "4.2.2",
17+
"@angular/compiler": "4.2.2",
18+
"@angular/compiler-cli": "4.2.2",
19+
"@angular/core": "4.2.2",
20+
"@angular/forms": "4.2.2",
21+
"@angular/http": "4.2.2",
2222
"@angular/material": "2.0.0-beta.3",
23-
"@angular/platform-browser": "4.0.0",
24-
"@angular/platform-browser-dynamic": "4.0.0",
25-
"@angular/router": "4.0.0",
26-
"@angular/tsc-wrapped": "4.0.0",
23+
"@angular/platform-browser": "4.2.2",
24+
"@angular/platform-browser-dynamic": "4.2.2",
25+
"@angular/router": "4.2.2",
26+
"@angular/tsc-wrapped": "4.2.2",
2727
"angular-ssr": "latest",
2828
"classlist.js": "^1.1.20150312",
2929
"core-js": "^2.4.1",

examples/demand-express/package.json

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,19 @@
1818
},
1919
"license": "BSD-2-Clause",
2020
"dependencies": {
21-
"@angular/animations": ">=4.0.0 <5.0.0",
21+
"@angular/animations": ">=4.2.2 <5.0.0",
2222
"@angular/cli": ">=1.0.0",
23-
"@angular/common": ">=4.0.0 <5.0.0",
24-
"@angular/compiler": ">=4.0.0 <5.0.0",
25-
"@angular/compiler-cli": ">=4.0.0 <5.0.0",
26-
"@angular/core": ">=4.0.0 <5.0.0",
27-
"@angular/forms": ">=4.0.0 <5.0.0",
28-
"@angular/http": ">=4.0.0 <5.0.0",
23+
"@angular/common": ">=4.2.2 <5.0.0",
24+
"@angular/compiler": ">=4.2.2 <5.0.0",
25+
"@angular/compiler-cli": ">=4.2.2 <5.0.0",
26+
"@angular/core": ">=4.2.2 <5.0.0",
27+
"@angular/forms": ">=4.2.2 <5.0.0",
28+
"@angular/http": ">=4.2.2 <5.0.0",
2929
"@angular/material": "2.0.0-beta.3",
30-
"@angular/platform-browser": ">=4.0.0 <5.0.0",
31-
"@angular/platform-browser-dynamic": ">=4.0.0 <5.0.0",
32-
"@angular/router": ">=4.0.0 <5.0.0",
33-
"@angular/tsc-wrapped": ">=4.0.0 <5.0.0",
30+
"@angular/platform-browser": ">=4.2.2 <5.0.0",
31+
"@angular/platform-browser-dynamic": ">=4.2.2 <5.0.0",
32+
"@angular/router": ">=4.2.2 <5.0.0",
33+
"@angular/tsc-wrapped": ">=4.2.2 <5.0.0",
3434
"@types/cookie-parser": "^1.3.30",
3535
"@types/express": "^4.0.35",
3636
"@types/node-fetch": "^1.6.7",

package.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "angular-ssr",
3-
"version": "0.10.14",
3+
"version": "0.10.16",
44
"description": "Angular server-side rendering implementation",
55
"main": "build/index.js",
66
"typings": "build/index.d.ts",
@@ -55,15 +55,15 @@
5555
},
5656
"peerDependencies": {
5757
"@angular/cli": ">=1.1.0",
58-
"@angular/common": ">=4.2.0 <5.0.0",
59-
"@angular/compiler": ">=4.2.0 <5.0.0",
60-
"@angular/compiler-cli": ">=4.2.0 <5.0.0",
61-
"@angular/core": ">=4.2.0 <5.0.0",
62-
"@angular/forms": ">=4.2.0 <5.0.0",
63-
"@angular/http": ">=4.2.0 <5.0.0",
64-
"@angular/platform-browser": ">=4.2.0 <5.0.0",
65-
"@angular/router": ">=4.2.0 <5.0.0",
66-
"@angular/tsc-wrapped": ">=4.2.0 <5.0.0",
58+
"@angular/common": ">=4.2.2 <5.0.0",
59+
"@angular/compiler": ">=4.2.2 <5.0.0",
60+
"@angular/compiler-cli": ">=4.2.2 <5.0.0",
61+
"@angular/core": ">=4.2.2 <5.0.0",
62+
"@angular/forms": ">=4.2.2 <5.0.0",
63+
"@angular/http": ">=4.2.2 <5.0.0",
64+
"@angular/platform-browser": ">=4.2.2 <5.0.0",
65+
"@angular/router": ">=4.2.2 <5.0.0",
66+
"@angular/tsc-wrapped": ">=4.2.2 <5.0.0",
6767
"@angular/service-worker": ">=1.0.0 || >=1.0.0-beta.8",
6868
"reflect-metadata": ">=0.1.10",
6969
"rxjs": ">=5.0.1",

source/bin/options/parse.ts

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
PathReference,
1717
PrebootConfiguration,
1818
Project,
19+
OutputOptions,
1920
OutputProducer,
2021
absoluteFile,
2122
fileFromString,
@@ -91,19 +92,15 @@ export const parseCommandLineOptions = (): CommandLineOptions => {
9192
let enablePreboot: boolean = false;
9293

9394
// Inline CSS resources in the compiled HTML output
94-
let enableInline: boolean = true;
95+
let inlineStylesheets: boolean = true;
96+
97+
// Inline SVG <use xlink:href> instances to deal with absolute paths that wrongly include localhost
98+
let inlineVectorGraphics: boolean = false;
9599

96100
// Enable 'blacklist by default' route rendering behaviour (each route you wish to render must be marked with `server: true')
97101
let blacklist: boolean = false;
98102

99-
const createOutput = (options): OutputProducer =>
100-
options['ipc']
101-
? createInterprocessOutput(options)
102-
: createHtmlOutput(options);
103-
104-
const createInterprocessOutput = (options): OutputProducer => new InterprocessOutput();
105-
106-
const createHtmlOutput = (options): OutputProducer => {
103+
const renderOptions = (options): OutputOptions => {
107104
let outputString = options['output'];
108105

109106
if (/^(\\|\/)/.test(outputString) === false) {
@@ -112,9 +109,18 @@ const createHtmlOutput = (options): OutputProducer => {
112109

113110
const output = pathFromString(outputString);
114111

115-
return new HtmlOutput(output, enableInline);
112+
return {output, inlineStylesheets, inlineVectorGraphics};
116113
};
117114

115+
const createOutput = (options): OutputProducer =>
116+
options['ipc']
117+
? createInterprocessOutput(renderOptions(options))
118+
: createHtmlOutput(renderOptions(options));
119+
120+
const createInterprocessOutput = (options: OutputOptions): OutputProducer => new InterprocessOutput(options);
121+
122+
const createHtmlOutput = (options: OutputOptions): OutputProducer => new HtmlOutput(options);
123+
118124
const parseCommandLine = () => {
119125
const options = commander
120126
.version(version)
@@ -130,14 +136,21 @@ const parseCommandLine = () => {
130136
.option('-a, --application <applicationID>', 'Optional application ID if your CLI configuration contains multiple apps')
131137
.option('-P, --preboot [boolean | json-file | json-text]', 'Enable or disable preboot with optional configuration file or JSON text (otherwise automatically find the root element and use defaults)')
132138
.option('-i, --inline [boolean]', 'Inline of resources referenced in links')
139+
.option('-S, --inline-svg [boolean]', 'Inline SVG <use xlink:href> instances (to resolve issues with absolute URI SVG identifiers eg http://localhost/#foo')
133140
.option('-I, --ipc', 'Send rendered documents to parent process through IPC instead of writing them to disk', false)
134141
.option('-b, --blacklist [boolean]', 'Blacklist all routes by default such that all routes which should be rendered must be specially marked with "server: true" in the route definition', false)
135142

136-
options.on('preboot', value => enablePreboot = value == null ? true : value);
143+
options.on('preboot',
144+
value => enablePreboot = value == null ? true : value);
145+
146+
options.on('inline',
147+
value => inlineStylesheets = value == null ? true : value);
137148

138-
options.on('inline', value => enableInline = value == null ? true : value);
149+
options.on('inline-svg',
150+
value => inlineVectorGraphics = value == null ? true : value);
139151

140-
options.on('blacklist', value => blacklist = value == null ? true : value);
152+
options.on('blacklist',
153+
value => blacklist = value == null ? true : value);
141154

142155
return options.parse(process.argv);
143156
};

source/output/html.ts

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,32 @@ import chalk = require('chalk');
33
import {join} from 'path';
44

55
import {Files} from '../static';
6+
import {OutputOptions} from './options';
67
import {OutputProducer} from './producer';
78
import {OutputException} from '../exception';
8-
import {PathReference, fileFromString, pathFromString} from '../filesystem';
9+
import {fileFromString, pathFromString} from '../filesystem';
910
import {Snapshot} from '../snapshot';
10-
import {inlineResources} from './inline';
1111
import {log} from './log';
1212
import {pathFromUri} from '../route';
13+
import {transformInplace} from './transform';
1314

1415
export class HtmlOutput implements OutputProducer {
15-
private path: PathReference;
16-
17-
constructor(path: PathReference | string, private inline: boolean) {
18-
this.path = pathFromString(path);
19-
}
16+
constructor(private options: OutputOptions) {}
2017

2118
initialize() {
22-
if (this.path == null) {
19+
if (this.options.output == null) {
2320
throw new OutputException('HTML output writer needs a path to write to');
2421
}
2522

26-
if (this.path.exists() === false) {
23+
if (this.options.output.exists() === false) {
2724
try {
28-
this.path.mkdir();
25+
this.options.output.mkdir();
2926
}
3027
catch (exception) {
31-
throw new OutputException(`Cannot create output folder: ${this.path}`, exception);
28+
throw new OutputException(`Cannot create output folder: ${this.options.output.toString()}`, exception);
3229
}
3330

34-
log.info(`Created output path: ${this.path}`);
31+
log.info(`Created output path: ${this.options.output.toString()}`);
3532
}
3633

3734
return Promise.resolve();
@@ -40,17 +37,11 @@ export class HtmlOutput implements OutputProducer {
4037
async write<V>(snapshot: Snapshot<V>): Promise<void> {
4138
const file = fileFromString(join(this.routedPathFromSnapshot(snapshot).toString(), Files.index));
4239

43-
let rendered = this.inline === true
44-
? inlineResources(file.parent(), snapshot.renderedDocument)
45-
: snapshot.renderedDocument;
46-
47-
if (/^<\!DOCTYPE html>/i.test(rendered) === false) { // ensure result has a doctype
48-
rendered = `<!DOCTYPE html>${rendered}`;
49-
}
40+
transformInplace(file.parent(), snapshot, this.options);
5041

5142
log.info(`Rendered route ${pathFromUri(snapshot.uri)} to ${file}`);
5243

53-
file.create(rendered);
44+
file.create(snapshot.renderedDocument);
5445

5546
return Promise.resolve(void 0);
5647
}
@@ -62,7 +53,7 @@ export class HtmlOutput implements OutputProducer {
6253
}
6354

6455
private routedPathFromSnapshot<V>(snapshot: Snapshot<V>) {
65-
const routedPath = pathFromString(join(this.path.toString(), pathFromUri(snapshot.uri)));
56+
const routedPath = pathFromString(join(this.options.output.toString(), pathFromUri(snapshot.uri)));
6657

6758
routedPath.mkdir();
6859

source/output/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export * from './html';
2-
export * from './inline';
32
export * from './interprocess';
43
export * from './log';
4+
export * from './options';
55
export * from './producer';

source/output/interprocess.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import {connected} from 'process';
22

33
import {RuntimeException} from '../exception';
4+
import {OutputOptions} from './options';
45
import {OutputProducer} from './producer';
56
import {Snapshot} from '../snapshot';
67

78
export class InterprocessOutput implements OutputProducer {
8-
constructor() {}
9+
constructor(options: OutputOptions) {}
910

1011
initialize() {
1112
if (connected === false) {

source/output/options.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {PathReference} from '../filesystem/contracts';
2+
3+
export interface OutputOptions {
4+
output: PathReference;
5+
6+
// Inline CSS stylesheets into the output
7+
inlineStylesheets: boolean;
8+
9+
// Inline SVG <use xlink:href> references to get around absolute path issues
10+
inlineVectorGraphics: boolean;
11+
}

source/output/inline.ts renamed to source/output/stylesheets.ts

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,26 @@
11
import {join} from 'path';
22

3-
import {ApplicationFallbackOptions} from '../static';
43
import {PathReference, fileFromString} from '../filesystem';
4+
55
import {RuntimeException} from '../exception';
6-
import {createModernWindow} from '../runtime/browser-emulation/create';
76

8-
export const inlineResources = (path: PathReference, rendered: string): string => {
7+
export const inlineStylesheets = (path: PathReference, document: Document): string => {
98
try {
10-
const uri = ApplicationFallbackOptions.fallbackUri;
11-
12-
const window = createModernWindow(rendered, uri);
13-
try {
14-
const links = Array.from(window.document.querySelectorAll('link[rel="stylesheet"]'));
15-
16-
for (const link of links) {
17-
const resource = readResource(window.document, path, link as HTMLLinkElement);
18-
if (resource == null) {
19-
continue;
20-
}
9+
const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'));
2110

22-
link.parentElement.replaceChild(resource, link);
11+
for (const link of links) {
12+
const resource = readResource(window.document, path, link as HTMLLinkElement);
13+
if (resource == null) {
14+
continue;
2315
}
2416

25-
return window.document.documentElement.outerHTML;
26-
}
27-
finally {
28-
window.close();
17+
link.parentElement.replaceChild(resource, link);
2918
}
19+
20+
return window.document.documentElement.outerHTML;
3021
}
3122
catch (exception) {
32-
throw new RuntimeException('Failed to inline resources', exception);
23+
throw new RuntimeException('Failed to inline stylesheet resources', exception);
3324
}
3425
};
3526

source/output/svg.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const getVectorDefinitions = (document: Document): Map<string, Element> => {
2+
const map = new Map<string, Element>();
3+
4+
for (const definition of Array.from(document.querySelectorAll(`svg > defs > *[id!='']`))) {
5+
const identifier = definition.getAttribute('id');
6+
if (identifier) {
7+
map.set(identifier, definition);
8+
}
9+
}
10+
11+
return map;
12+
};
13+
14+
export const inlineVectorGraphics = (document: Document): void => {
15+
const definitions = getVectorDefinitions(document);
16+
17+
const links = Array.from(document.querySelectorAll('svg > use'));
18+
19+
for (const link of links) {
20+
const identifier = link.getAttribute('xlink:href');
21+
22+
const matchingDefinition = definitions.get(identifier.split(/[#\/]/g).pop());
23+
if (matchingDefinition == null) {
24+
throw new Error(`Cannot find matching SVG definition for ${identifier}`);
25+
}
26+
27+
link.parentElement.replaceChild(matchingDefinition.cloneNode(true), link);
28+
}
29+
};

source/output/transform.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import {ApplicationFallbackOptions} from '../static';
2+
import {OutputOptions} from './options';
3+
import {PathReference} from '../filesystem/contracts';
4+
import {Snapshot} from '../snapshot/snapshot';
5+
6+
import {createModernWindow} from '../runtime/browser-emulation/create';
7+
import {inlineStylesheets} from './stylesheets';
8+
import {inlineVectorGraphics} from './svg';
9+
10+
export const transformInplace = <V>(path: PathReference, snapshot: Snapshot<V>, options: OutputOptions): void => {
11+
if (options.inlineStylesheets || options.inlineVectorGraphics) {
12+
const uri = ApplicationFallbackOptions.fallbackUri;
13+
14+
const window = createModernWindow(snapshot.renderedDocument, uri);
15+
16+
try {
17+
if (options.inlineStylesheets) {
18+
inlineStylesheets(path, window.document);
19+
}
20+
21+
if (options.inlineVectorGraphics) {
22+
inlineVectorGraphics(window.document);
23+
}
24+
25+
snapshot.renderedDocument = document.documentElement.outerHTML;
26+
}
27+
finally {
28+
window.close();
29+
}
30+
}
31+
32+
if (/^<\!DOCTYPE html>/i.test(snapshot.renderedDocument) === false) { // ensure result has a doctype
33+
snapshot.renderedDocument = `<!DOCTYPE html>${snapshot.renderedDocument}`;
34+
}
35+
};

0 commit comments

Comments
 (0)