Skip to content

Commit 392a393

Browse files
Charles Lydinghansl
Charles Lyding
authored andcommitted
feat(@angular/cli): support inlining SVG within stylesheets
1 parent 4774049 commit 392a393

File tree

7 files changed

+113
-53
lines changed

7 files changed

+113
-53
lines changed

packages/@angular/cli/models/webpack-configs/styles.ts

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export function getStylesConfig(wco: WebpackConfigOptions) {
4949

5050
const postcssPluginCreator = function() {
5151
// safe settings based on: https://github.com/ben-eb/cssnano/issues/358#issuecomment-283696193
52-
const importantCommentRe = /@preserve|@license|[@#]\s*source(?:Mapping)?URL|^!/i;
52+
const importantCommentRe = /@preserve|@licen[cs]e|[@#]\s*source(?:Mapping)?URL|^!/i;
5353
const minimizeOptions = {
5454
autoprefixer: false, // full pass with autoprefixer is run separately
5555
safe: true,
@@ -58,30 +58,35 @@ export function getStylesConfig(wco: WebpackConfigOptions) {
5858
};
5959

6060
return [
61-
postcssUrl({
62-
url: (URL: { url: string }) => {
63-
const { url } = URL;
61+
postcssUrl([
62+
{
6463
// Only convert root relative URLs, which CSS-Loader won't process into require().
65-
if (!url.startsWith('/') || url.startsWith('//')) {
66-
return URL.url;
67-
}
68-
69-
if (deployUrl.match(/:\/\//)) {
70-
// If deployUrl contains a scheme, ignore baseHref use deployUrl as is.
71-
return `${deployUrl.replace(/\/$/, '')}${url}`;
72-
} else if (baseHref.match(/:\/\//)) {
73-
// If baseHref contains a scheme, include it as is.
74-
return baseHref.replace(/\/$/, '') +
75-
`/${deployUrl}/${url}`.replace(/\/\/+/g, '/');
76-
} else {
77-
// Join together base-href, deploy-url and the original URL.
78-
// Also dedupe multiple slashes into single ones.
79-
return `/${baseHref}/${deployUrl}/${url}`.replace(/\/\/+/g, '/');
64+
filter: ({ url }: { url: string }) => url.startsWith('/') && !url.startsWith('//'),
65+
url: ({ url }: { url: string }) => {
66+
if (deployUrl.match(/:\/\//)) {
67+
// If deployUrl contains a scheme, ignore baseHref use deployUrl as is.
68+
return `${deployUrl.replace(/\/$/, '')}${url}`;
69+
} else if (baseHref.match(/:\/\//)) {
70+
// If baseHref contains a scheme, include it as is.
71+
return baseHref.replace(/\/$/, '') +
72+
`/${deployUrl}/${url}`.replace(/\/\/+/g, '/');
73+
} else {
74+
// Join together base-href, deploy-url and the original URL.
75+
// Also dedupe multiple slashes into single ones.
76+
return `/${baseHref}/${deployUrl}/${url}`.replace(/\/\/+/g, '/');
77+
}
8078
}
79+
},
80+
{
81+
// TODO: inline .cur if not supporting IE (use browserslist to check)
82+
filter: (asset: any) => !asset.hash && !asset.absolutePath.endsWith('.cur'),
83+
url: 'inline',
84+
// NOTE: maxSize is in KB
85+
maxSize: 10
8186
}
82-
}),
87+
]),
8388
autoprefixer(),
84-
customProperties({ preserve: true})
89+
customProperties({ preserve: true })
8590
].concat(
8691
minimizeCss ? [cssnano(minimizeOptions)] : []
8792
);
@@ -164,7 +169,7 @@ export function getStylesConfig(wco: WebpackConfigOptions) {
164169
loader: 'css-loader',
165170
options: {
166171
sourceMap: cssSourceMap,
167-
importLoaders: 1
172+
importLoaders: 1,
168173
}
169174
},
170175
{

tests/e2e/assets/images/spectrum.png

30.1 KB
Loading

tests/e2e/tests/build/css-urls.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import * as fs from 'fs';
21
import { ng } from '../../utils/process';
32
import {
43
expectFileToMatch,
54
expectFileToExist,
65
expectFileMatchToExist,
76
writeMultipleFiles
87
} from '../../utils/fs';
8+
import { copyProjectAsset } from '../../utils/assets';
99
import { expectToFail } from '../../utils/utils';
1010
import { getGlobalVariable } from '../../utils/env';
1111

@@ -26,32 +26,34 @@ export default function () {
2626
.then(() => writeMultipleFiles({
2727
'src/styles.css': `
2828
h1 { background: url('/assets/global-img-absolute.svg'); }
29-
h2 { background: url('./assets/global-img-relative.svg'); }
29+
h2 { background: url('./assets/global-img-relative.png'); }
3030
`,
3131
'src/app/app.component.css': `
3232
h3 { background: url('/assets/component-img-absolute.svg'); }
33-
h4 { background: url('../assets/component-img-relative.svg'); }
33+
h4 { background: url('../assets/component-img-relative.png'); }
3434
`,
35-
// Using SVGs because they are loaded via file-loader and thus never inlined.
3635
'src/assets/global-img-absolute.svg': imgSvg,
37-
'src/assets/global-img-relative.svg': imgSvg,
38-
'src/assets/component-img-absolute.svg': imgSvg,
39-
'src/assets/component-img-relative.svg': imgSvg
36+
'src/assets/component-img-absolute.svg': imgSvg
4037
}))
38+
// use image with file size >10KB to prevent inlining
39+
.then(() => copyProjectAsset('images/spectrum.png', './assets/global-img-relative.png'))
40+
.then(() => copyProjectAsset('images/spectrum.png', './assets/component-img-relative.png'))
4141
.then(() => ng('build', '--extract-css', '--aot'))
4242
// Check paths are correctly generated.
4343
.then(() => expectFileToMatch('dist/styles.bundle.css', '/assets/global-img-absolute.svg'))
4444
.then(() => expectFileToMatch('dist/styles.bundle.css',
45-
/global-img-relative\.[0-9a-f]{20}\.svg/))
45+
/url\('\/assets\/global-img-absolute\.svg'\)/))
46+
.then(() => expectFileToMatch('dist/styles.bundle.css',
47+
/global-img-relative\.[0-9a-f]{20}\.png/))
4648
.then(() => expectFileToMatch('dist/main.bundle.js',
4749
'/assets/component-img-absolute.svg'))
4850
.then(() => expectFileToMatch('dist/main.bundle.js',
49-
/component-img-relative\.[0-9a-f]{20}\.svg/))
51+
/component-img-relative\.[0-9a-f]{20}\.png/))
5052
// Check files are correctly created.
5153
.then(() => expectToFail(() => expectFileToExist('dist/global-img-absolute.svg')))
5254
.then(() => expectToFail(() => expectFileToExist('dist/component-img-absolute.svg')))
53-
.then(() => expectFileMatchToExist('./dist', /global-img-relative\.[0-9a-f]{20}\.svg/))
54-
.then(() => expectFileMatchToExist('./dist', /component-img-relative\.[0-9a-f]{20}\.svg/))
55+
.then(() => expectFileMatchToExist('./dist', /global-img-relative\.[0-9a-f]{20}\.png/))
56+
.then(() => expectFileMatchToExist('./dist', /component-img-relative\.[0-9a-f]{20}\.png/))
5557
// Check urls with deploy-url scheme are used as is.
5658
.then(() => ng('build', '--base-href=/base/', '--deploy-url=http://deploy.url/',
5759
'--extract-css'))
@@ -79,9 +81,9 @@ export default function () {
7981
.then(() => expectFileToMatch('dist/styles.bundle.css',
8082
'/base/deploy/assets/global-img-absolute.svg'))
8183
.then(() => expectFileToMatch('dist/styles.bundle.css',
82-
/global-img-relative\.[0-9a-f]{20}\.svg/))
84+
/global-img-relative\.[0-9a-f]{20}\.png/))
8385
.then(() => expectFileToMatch('dist/main.bundle.js',
8486
'/base/deploy/assets/component-img-absolute.svg'))
8587
.then(() => expectFileToMatch('dist/main.bundle.js',
86-
/deploy\/component-img-relative\.[0-9a-f]{20}\.svg/));
88+
/deploy\/component-img-relative\.[0-9a-f]{20}\.png/));
8789
}

tests/e2e/tests/build/deploy-url.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { ng } from '../../utils/process';
2+
import { copyProjectAsset } from '../../utils/assets';
23
import { expectFileToMatch, writeMultipleFiles } from '../../utils/fs';
34
import { updateJsonFile } from '../../utils/project';
45
import { getGlobalVariable } from '../../utils/env';
5-
import { stripIndents } from 'common-tags';
66

77

88
export default function () {
@@ -14,16 +14,14 @@ export default function () {
1414

1515
return Promise.resolve()
1616
.then(() => writeMultipleFiles({
17-
'src/styles.css': 'div { background: url("./assets/more.svg"); }',
18-
'src/assets/more.svg': stripIndents`
19-
<svg width="100" height="100">
20-
<circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
21-
</svg>
22-
`}))
17+
'src/styles.css': 'div { background: url("./assets/more.png"); }',
18+
}))
19+
// use image with file size >10KB to prevent inlining
20+
.then(() => copyProjectAsset('images/spectrum.png', './assets/more.png'))
2321
.then(() => ng('build', '--deploy-url=deployUrl/', '--extract-css'))
2422
.then(() => expectFileToMatch('dist/index.html', 'deployUrl/main.bundle.js'))
2523
// verify --deploy-url isn't applied to extracted css urls
26-
.then(() => expectFileToMatch('dist/styles.bundle.css', /url\(more\.[0-9a-f]{20}\.svg\)/))
24+
.then(() => expectFileToMatch('dist/styles.bundle.css', /url\(more\.[0-9a-f]{20}\.png\)/))
2725
.then(() => ng('build', '--deploy-url=http://example.com/some/path/', '--extract-css'))
2826
.then(() => expectFileToMatch('dist/index.html', 'http://example.com/some/path/main.bundle.js'))
2927
// verify option also works in config
@@ -36,5 +34,5 @@ export default function () {
3634
// verify --deploy-url is applied to non-extracted css urls
3735
.then(() => ng('build', '--deploy-url=deployUrl/', '--extract-css=false'))
3836
.then(() => expectFileToMatch('dist/styles.bundle.js',
39-
/__webpack_require__.p \+ \"more\.[0-9a-f]{20}\.svg\"/));
37+
/__webpack_require__.p \+ \"more\.[0-9a-f]{20}\.png\"/));
4038
}
Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import {stripIndents} from 'common-tags';
21
import {ng} from '../../utils/process';
2+
import { copyProjectAsset } from '../../utils/assets';
33
import { writeMultipleFiles, expectFileToMatch, expectFileMatchToExist } from '../../utils/fs';
44
import { getGlobalVariable } from '../../utils/env';
55

@@ -16,32 +16,31 @@ export default function() {
1616

1717
return Promise.resolve()
1818
.then(() => writeMultipleFiles({
19-
'src/styles.css': stripIndents`
20-
body { background-image: url("image.svg"); }
21-
`,
22-
'src/image.svg': 'I would like to be an image someday.'
19+
'src/styles.css': 'body { background-image: url("./assets/image.png"); }'
2320
}))
21+
// use image with file size >10KB to prevent inlining
22+
.then(() => copyProjectAsset('images/spectrum.png', './assets/image.png'))
2423
.then(() => ng('build', '--dev', '--output-hashing=all'))
2524
.then(() => expectFileToMatch('dist/index.html', /inline\.[0-9a-f]{20}\.bundle\.js/))
2625
.then(() => expectFileToMatch('dist/index.html', /main\.[0-9a-f]{20}\.bundle\.js/))
2726
.then(() => expectFileToMatch('dist/index.html', /styles\.[0-9a-f]{20}\.bundle\.(css|js)/))
28-
.then(() => verifyMedia(/styles\.[0-9a-f]{20}\.bundle\.(css|js)/, /image\.[0-9a-f]{20}\.svg/))
27+
.then(() => verifyMedia(/styles\.[0-9a-f]{20}\.bundle\.(css|js)/, /image\.[0-9a-f]{20}\.png/))
2928

3029
.then(() => ng('build', '--prod', '--output-hashing=none'))
3130
.then(() => expectFileToMatch('dist/index.html', /inline\.bundle\.js/))
3231
.then(() => expectFileToMatch('dist/index.html', /main\.bundle\.js/))
3332
.then(() => expectFileToMatch('dist/index.html', /styles\.bundle\.(css|js)/))
34-
.then(() => verifyMedia(/styles\.bundle\.(css|js)/, /image\.svg/))
33+
.then(() => verifyMedia(/styles\.bundle\.(css|js)/, /image\.png/))
3534

3635
.then(() => ng('build', '--dev', '--output-hashing=media'))
3736
.then(() => expectFileToMatch('dist/index.html', /inline\.bundle\.js/))
3837
.then(() => expectFileToMatch('dist/index.html', /main\.bundle\.js/))
3938
.then(() => expectFileToMatch('dist/index.html', /styles\.bundle\.(css|js)/))
40-
.then(() => verifyMedia(/styles\.bundle\.(css|js)/, /image\.[0-9a-f]{20}\.svg/))
39+
.then(() => verifyMedia(/styles\.bundle\.(css|js)/, /image\.[0-9a-f]{20}\.png/))
4140

4241
.then(() => ng('build', '--dev', '--output-hashing=bundles'))
4342
.then(() => expectFileToMatch('dist/index.html', /inline\.[0-9a-f]{20}\.bundle\.js/))
4443
.then(() => expectFileToMatch('dist/index.html', /main\.[0-9a-f]{20}\.bundle\.js/))
4544
.then(() => expectFileToMatch('dist/index.html', /styles\.[0-9a-f]{20}\.bundle\.(css|js)/))
46-
.then(() => verifyMedia(/styles\.[0-9a-f]{20}\.bundle\.(css|js)/, /image\.svg/));
45+
.then(() => verifyMedia(/styles\.[0-9a-f]{20}\.bundle\.(css|js)/, /image\.png/));
4746
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { ng } from '../../../utils/process';
2+
import {
3+
expectFileToMatch,
4+
expectFileToExist,
5+
expectFileMatchToExist,
6+
writeMultipleFiles
7+
} from '../../../utils/fs';
8+
import { copyProjectAsset } from '../../../utils/assets';
9+
import { expectToFail } from '../../../utils/utils';
10+
11+
const imgSvg = `
12+
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
13+
<circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
14+
</svg>
15+
`;
16+
17+
export default function () {
18+
return Promise.resolve()
19+
.then(() => writeMultipleFiles({
20+
'src/styles.css': `
21+
h1 { background: url('./assets/large.png'); }
22+
h2 { background: url('./assets/small.svg'); }
23+
p { background: url('./assets/small-id.svg#testID'); }
24+
`,
25+
'src/app/app.component.css': `
26+
h3 { background: url('../assets/small.svg'); }
27+
h4 { background: url('../assets/large.png'); }
28+
`,
29+
'src/assets/small.svg': imgSvg,
30+
'src/assets/small-id.svg': imgSvg
31+
}))
32+
.then(() => copyProjectAsset('images/spectrum.png', './assets/large.png'))
33+
.then(() => ng('build', '--extract-css', '--aot'))
34+
// Check paths are correctly generated.
35+
.then(() => expectFileToMatch('dist/styles.bundle.css',
36+
/url\([\'"]?large\.[0-9a-f]{20}\.png[\'"]?\)/))
37+
.then(() => expectFileToMatch('dist/styles.bundle.css',
38+
/url\(\\?[\'"]data:image\/svg\+xml/))
39+
.then(() => expectFileToMatch('dist/styles.bundle.css',
40+
/url\([\'"]?small-id\.[0-9a-f]{20}\.svg#testID[\'"]?\)/))
41+
.then(() => expectFileToMatch('dist/main.bundle.js',
42+
/url\(\\?[\'"]data:image\/svg\+xml/))
43+
.then(() => expectFileToMatch('dist/main.bundle.js',
44+
/url\([\'"]?large\.[0-9a-f]{20}\.png[\'"]?\)/))
45+
// Check files are correctly created.
46+
.then(() => expectToFail(() => expectFileToExist('dist/small.svg')))
47+
.then(() => expectFileMatchToExist('./dist', /large\.[0-9a-f]{20}\.png/))
48+
.then(() => expectFileMatchToExist('./dist', /small-id\.[0-9a-f]{20}\.svg/));
49+
}

tests/e2e/utils/assets.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ export function assetDir(assetName: string) {
1111
return join(__dirname, '../assets', assetName);
1212
}
1313

14+
export function copyProjectAsset(assetName: string, to?: string) {
15+
const tempRoot = join(getGlobalVariable('tmp-root'), 'test-project', 'src');
16+
const sourcePath = assetDir(assetName);
17+
const targetPath = join(tempRoot, to || assetName);
18+
19+
return copyFile(sourcePath, targetPath);
20+
}
1421

1522
export function copyAssets(assetName: string) {
1623
const tempRoot = join(getGlobalVariable('tmp-root'), 'assets', assetName);

0 commit comments

Comments
 (0)