Skip to content

Commit 94b087c

Browse files
clydinKeen Yee Liau
authored and
Keen Yee Liau
committed
fix(@angular-devkit/build-angular): disable by default stylesheet root relative URL rebasing
BREAKING CHANGE: Root relative URLs are a standardized method to reference a resource path from the root of a host. The previous behavior of the Angular CLI prevented this from occuring and resulted in an inability to reference stylesheet assets in this manner. The initial reason for this behavior is no longer present in the internal implementation of the Angular CLI. Therefore, this now unnecessary and non-standard behavior is being phased out. If an application currently relies on this behavior, a compatibility option `rebaseRootRelativeCssUrls` has been provided for the 8.x release cycle to facilitate transition away from this non-standard and limiting behavior. The recommended method to transition is to use relative paths within the source stylesheet. This allows the build system to process and generate a full URL for the asset.
1 parent 9f33253 commit 94b087c

File tree

8 files changed

+176
-26
lines changed

8 files changed

+176
-26
lines changed

packages/angular/cli/lib/config/schema.json

+6
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,12 @@
875875
"description": "Enables conditionally loaded ES2015 polyfills.",
876876
"type": "boolean",
877877
"default": false
878+
},
879+
"rebaseRootRelativeCssUrls": {
880+
"description": "Change root relative URLs in stylesheets to include base HREF and deploy URL. Use only for compatibility and transition. The behavior of this option is non-standard and will be removed in the next major release.",
881+
"type": "boolean",
882+
"default": false,
883+
"x-deprecated": true
878884
}
879885
},
880886
"additionalProperties": false,

packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts

+2
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ export interface BuildOptions {
7171
lazyModules: string[];
7272
platform?: 'browser' | 'server';
7373
fileReplacements: CurrentFileReplacement[];
74+
/** @deprecated use only for compatibility in 8.x; will be removed in 9.0 */
75+
rebaseRootRelativeCssUrls?: boolean;
7476
}
7577

7678
export interface WebpackTestOptions extends BuildOptions {

packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/styles.ts

+4-7
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,6 @@ export function getStylesConfig(wco: WebpackConfigOptions) {
4545

4646
// Determine hashing format.
4747
const hashFormat = getOutputHashFormat(buildOptions.outputHashing as string);
48-
// Convert absolute resource URLs to account for base-href and deploy-url.
49-
const baseHref = buildOptions.baseHref || '';
50-
const deployUrl = buildOptions.deployUrl || '';
51-
const resourcesOutputPath = buildOptions.resourcesOutputPath || '';
5248

5349
const postcssPluginCreator = function (loader: webpack.loader.LoaderContext) {
5450
return [
@@ -70,10 +66,11 @@ export function getStylesConfig(wco: WebpackConfigOptions) {
7066
},
7167
}),
7268
PostcssCliResources({
73-
baseHref,
74-
deployUrl,
75-
resourcesOutputPath,
69+
baseHref: buildOptions.baseHref,
70+
deployUrl: buildOptions.deployUrl,
71+
resourcesOutputPath: buildOptions.resourcesOutputPath,
7672
loader,
73+
rebaseRootRelative: buildOptions.rebaseRootRelativeCssUrls,
7774
filename: `[name]${hashFormat.file}.[ext]`,
7875
}),
7976
autoprefixer(),

packages/angular_devkit/build_angular/src/angular-cli-files/plugins/postcss-cli-resources.ts

+6
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface PostcssCliResourcesOptions {
2828
baseHref?: string;
2929
deployUrl?: string;
3030
resourcesOutputPath?: string;
31+
rebaseRootRelative?: boolean;
3132
filename: string;
3233
loader: webpack.loader.LoaderContext;
3334
}
@@ -49,6 +50,7 @@ export default postcss.plugin('postcss-cli-resources', (options: PostcssCliResou
4950
deployUrl = '',
5051
baseHref = '',
5152
resourcesOutputPath = '',
53+
rebaseRootRelative = false,
5254
filename,
5355
loader,
5456
} = options;
@@ -61,6 +63,10 @@ export default postcss.plugin('postcss-cli-resources', (options: PostcssCliResou
6163
return inputUrl;
6264
}
6365

66+
if (!rebaseRootRelative && /^\//.test(inputUrl)) {
67+
return inputUrl;
68+
}
69+
6470
// If starts with a caret, remove and return remainder
6571
// this supports bypassing asset processing
6672
if (inputUrl.startsWith('^')) {

packages/angular_devkit/build_angular/src/browser/schema.d.ts

+8
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,14 @@ export interface BrowserBuilderSchema {
241241
* Enables conditionally loaded IE9-11 polyfills.
242242
*/
243243
es5BrowserSupport: boolean;
244+
245+
/**
246+
* @deprecated
247+
* Change root relative URLs in stylesheets to include base HREF and deploy URL.
248+
* Use only for compatibility and transition.
249+
* The behavior of this option is non-standard and will be removed in the next major release.
250+
*/
251+
rebaseRootRelativeCssUrls: boolean;
244252
}
245253

246254
export type OptimizationOptions = boolean | OptimizationObject;

packages/angular_devkit/build_angular/src/browser/schema.json

+6
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,12 @@
304304
"description": "Enables conditionally loaded ES2015 polyfills.",
305305
"type": "boolean",
306306
"default": false
307+
},
308+
"rebaseRootRelativeCssUrls": {
309+
"description": "Change root relative URLs in stylesheets to include base HREF and deploy URL. Use only for compatibility and transition. The behavior of this option is non-standard and will be removed in the next major release.",
310+
"type": "boolean",
311+
"default": false,
312+
"x-deprecated": true
307313
}
308314
},
309315
"additionalProperties": false,

packages/angular_devkit/build_angular/test/browser/styles_spec_large.ts

+132-6
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ describe('Browser Builder styles', () => {
352352
});
353353

354354
// TODO: consider making this a unit test in the url processing plugins.
355-
it(`supports baseHref and deployUrl in resource urls`, (done) => {
355+
it(`supports baseHref/deployUrl in resource urls without rebaseRootRelativeCssUrls`, (done) => {
356356
// Use a large image for the relative ref so it cannot be inlined.
357357
host.copyFile('src/spectrum.png', './src/assets/global-img-relative.png');
358358
host.copyFile('src/spectrum.png', './src/assets/component-img-relative.png');
@@ -396,6 +396,132 @@ describe('Browser Builder styles', () => {
396396
concatMap(() => runTargetSpec(host, browserTargetSpec,
397397
{ extractCss: true, baseHref: '/base/', deployUrl: 'http://deploy.url/' },
398398
)),
399+
tap(() => {
400+
const styles = virtualFs.fileBufferToString(
401+
host.scopedSync().read(normalize(stylesBundle)),
402+
);
403+
const main = virtualFs.fileBufferToString(host.scopedSync().read(normalize(mainBundle)));
404+
expect(styles)
405+
.toContain(`url('/assets/global-img-absolute.svg')`);
406+
expect(main)
407+
.toContain(`url('/assets/component-img-absolute.svg')`);
408+
}),
409+
// Check urls with base-href scheme are used as is (with deploy-url).
410+
concatMap(() => runTargetSpec(host, browserTargetSpec,
411+
{ extractCss: true, baseHref: 'http://base.url/', deployUrl: 'deploy/' },
412+
)),
413+
tap(() => {
414+
const styles = virtualFs.fileBufferToString(
415+
host.scopedSync().read(normalize(stylesBundle)),
416+
);
417+
const main = virtualFs.fileBufferToString(host.scopedSync().read(normalize(mainBundle)));
418+
expect(styles)
419+
.toContain(`url('/assets/global-img-absolute.svg')`);
420+
expect(main)
421+
.toContain(`url('/assets/component-img-absolute.svg')`);
422+
}),
423+
// Check urls with deploy-url and base-href scheme only use deploy-url.
424+
concatMap(() => runTargetSpec(host, browserTargetSpec, {
425+
extractCss: true,
426+
baseHref: 'http://base.url/',
427+
deployUrl: 'http://deploy.url/',
428+
},
429+
)),
430+
tap(() => {
431+
const styles = virtualFs.fileBufferToString(
432+
host.scopedSync().read(normalize(stylesBundle)),
433+
);
434+
const main = virtualFs.fileBufferToString(host.scopedSync().read(normalize(mainBundle)));
435+
expect(styles).toContain(`url('/assets/global-img-absolute.svg')`);
436+
expect(main).toContain(`url('/assets/component-img-absolute.svg')`);
437+
}),
438+
// Check with schemeless base-href and deploy-url flags.
439+
concatMap(() => runTargetSpec(host, browserTargetSpec,
440+
{ extractCss: true, baseHref: '/base/', deployUrl: 'deploy/' },
441+
)),
442+
tap(() => {
443+
const styles = virtualFs.fileBufferToString(
444+
host.scopedSync().read(normalize(stylesBundle)),
445+
);
446+
const main = virtualFs.fileBufferToString(host.scopedSync().read(normalize(mainBundle)));
447+
expect(styles).toContain(`url('/assets/global-img-absolute.svg')`);
448+
expect(main).toContain(`url('/assets/component-img-absolute.svg')`);
449+
}),
450+
// Check with identical base-href and deploy-url flags.
451+
concatMap(() => runTargetSpec(host, browserTargetSpec,
452+
{ extractCss: true, baseHref: '/base/', deployUrl: '/base/' },
453+
)),
454+
tap(() => {
455+
const styles = virtualFs.fileBufferToString(
456+
host.scopedSync().read(normalize(stylesBundle)),
457+
);
458+
const main = virtualFs.fileBufferToString(host.scopedSync().read(normalize(mainBundle)));
459+
expect(styles).toContain(`url('/assets/global-img-absolute.svg')`);
460+
expect(main).toContain(`url('/assets/component-img-absolute.svg')`);
461+
}),
462+
// Check with only base-href flag.
463+
concatMap(() => runTargetSpec(host, browserTargetSpec,
464+
{ extractCss: true, baseHref: '/base/' },
465+
)),
466+
tap(() => {
467+
const styles = virtualFs.fileBufferToString(
468+
host.scopedSync().read(normalize(stylesBundle)),
469+
);
470+
const main = virtualFs.fileBufferToString(host.scopedSync().read(normalize(mainBundle)));
471+
expect(styles).toContain(`url('/assets/global-img-absolute.svg')`);
472+
expect(main).toContain(`url('/assets/component-img-absolute.svg')`);
473+
}),
474+
).toPromise().then(done, done.fail);
475+
}, 90000);
476+
477+
it(`supports baseHref/deployUrl in resource urls with rebaseRootRelativeCssUrls`, (done) => {
478+
// Use a large image for the relative ref so it cannot be inlined.
479+
host.copyFile('src/spectrum.png', './src/assets/global-img-relative.png');
480+
host.copyFile('src/spectrum.png', './src/assets/component-img-relative.png');
481+
host.writeMultipleFiles({
482+
'src/styles.css': `
483+
h1 { background: url('/assets/global-img-absolute.svg'); }
484+
h2 { background: url('./assets/global-img-relative.png'); }
485+
`,
486+
'src/app/app.component.css': `
487+
h3 { background: url('/assets/component-img-absolute.svg'); }
488+
h4 { background: url('../assets/component-img-relative.png'); }
489+
`,
490+
'src/assets/global-img-absolute.svg': imgSvg,
491+
'src/assets/component-img-absolute.svg': imgSvg,
492+
});
493+
494+
const stylesBundle = 'dist/styles.css';
495+
const mainBundle = 'dist/main.js';
496+
497+
// Check base paths are correctly generated.
498+
const overrides = {
499+
extractCss: true,
500+
rebaseRootRelativeCssUrls: true,
501+
};
502+
runTargetSpec(host, browserTargetSpec, { ...overrides, aot: true }).pipe(
503+
tap(() => {
504+
const styles = virtualFs.fileBufferToString(
505+
host.scopedSync().read(normalize(stylesBundle)),
506+
);
507+
const main = virtualFs.fileBufferToString(host.scopedSync().read(normalize(mainBundle)));
508+
expect(styles).toContain(`url('/assets/global-img-absolute.svg')`);
509+
expect(styles).toContain(`url('global-img-relative.png')`);
510+
expect(main).toContain(`url('/assets/component-img-absolute.svg')`);
511+
expect(main).toContain(`url('component-img-relative.png')`);
512+
expect(host.scopedSync().exists(normalize('dist/assets/global-img-absolute.svg')))
513+
.toBe(true);
514+
expect(host.scopedSync().exists(normalize('dist/global-img-relative.png')))
515+
.toBe(true);
516+
expect(host.scopedSync().exists(normalize('dist/assets/component-img-absolute.svg')))
517+
.toBe(true);
518+
expect(host.scopedSync().exists(normalize('dist/component-img-relative.png')))
519+
.toBe(true);
520+
}),
521+
// Check urls with deploy-url scheme are used as is.
522+
concatMap(() => runTargetSpec(host, browserTargetSpec,
523+
{ ...overrides, baseHref: '/base/', deployUrl: 'http://deploy.url/' },
524+
)),
399525
tap(() => {
400526
const styles = virtualFs.fileBufferToString(
401527
host.scopedSync().read(normalize(stylesBundle)),
@@ -408,7 +534,7 @@ describe('Browser Builder styles', () => {
408534
}),
409535
// Check urls with base-href scheme are used as is (with deploy-url).
410536
concatMap(() => runTargetSpec(host, browserTargetSpec,
411-
{ extractCss: true, baseHref: 'http://base.url/', deployUrl: 'deploy/' },
537+
{ ...overrides, baseHref: 'http://base.url/', deployUrl: 'deploy/' },
412538
)),
413539
tap(() => {
414540
const styles = virtualFs.fileBufferToString(
@@ -422,7 +548,7 @@ describe('Browser Builder styles', () => {
422548
}),
423549
// Check urls with deploy-url and base-href scheme only use deploy-url.
424550
concatMap(() => runTargetSpec(host, browserTargetSpec, {
425-
extractCss: true,
551+
...overrides,
426552
baseHref: 'http://base.url/',
427553
deployUrl: 'http://deploy.url/',
428554
},
@@ -437,7 +563,7 @@ describe('Browser Builder styles', () => {
437563
}),
438564
// Check with schemeless base-href and deploy-url flags.
439565
concatMap(() => runTargetSpec(host, browserTargetSpec,
440-
{ extractCss: true, baseHref: '/base/', deployUrl: 'deploy/' },
566+
{ ...overrides, baseHref: '/base/', deployUrl: 'deploy/' },
441567
)),
442568
tap(() => {
443569
const styles = virtualFs.fileBufferToString(
@@ -449,7 +575,7 @@ describe('Browser Builder styles', () => {
449575
}),
450576
// Check with identical base-href and deploy-url flags.
451577
concatMap(() => runTargetSpec(host, browserTargetSpec,
452-
{ extractCss: true, baseHref: '/base/', deployUrl: '/base/' },
578+
{ ...overrides, baseHref: '/base/', deployUrl: '/base/' },
453579
)),
454580
tap(() => {
455581
const styles = virtualFs.fileBufferToString(
@@ -461,7 +587,7 @@ describe('Browser Builder styles', () => {
461587
}),
462588
// Check with only base-href flag.
463589
concatMap(() => runTargetSpec(host, browserTargetSpec,
464-
{ extractCss: true, baseHref: '/base/' },
590+
{ ...overrides, baseHref: '/base/' },
465591
)),
466592
tap(() => {
467593
const styles = virtualFs.fileBufferToString(

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

+12-13
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ export default function () {
2929
'src/assets/global-img-absolute.svg': imgSvg,
3030
'src/assets/component-img-absolute.svg': imgSvg
3131
}))
32-
// use image with file size >10KB to prevent inlining
3332
.then(() => copyProjectAsset('images/spectrum.png', './src/assets/global-img-relative.png'))
3433
.then(() => copyProjectAsset('images/spectrum.png', './src/assets/component-img-relative.png'))
3534
.then(() => ng('build', '--extract-css', '--aot'))
@@ -52,54 +51,54 @@ export default function () {
5251
.then(() => ng('build', '--base-href=/base/', '--deploy-url=http://deploy.url/',
5352
'--extract-css'))
5453
.then(() => expectFileToMatch('dist/test-project/styles.css',
55-
/url\(\'http:\/\/deploy\.url\/assets\/global-img-absolute\.svg\'\)/))
54+
/url\(\'\/assets\/global-img-absolute\.svg\'\)/))
5655
.then(() => expectFileToMatch('dist/test-project/main.js',
57-
/url\(\'http:\/\/deploy\.url\/assets\/component-img-absolute\.svg\'\)/))
56+
/url\(\'\/assets\/component-img-absolute\.svg\'\)/))
5857
// Check urls with base-href scheme are used as is (with deploy-url).
5958
.then(() => ng('build', '--base-href=http://base.url/', '--deploy-url=deploy/',
6059
'--extract-css'))
6160
.then(() => expectFileToMatch('dist/test-project/styles.css',
62-
/url\(\'http:\/\/base\.url\/deploy\/assets\/global-img-absolute\.svg\'\)/))
61+
/url\(\'\/assets\/global-img-absolute\.svg\'\)/))
6362
.then(() => expectFileToMatch('dist/test-project/main.js',
64-
/url\(\'http:\/\/base\.url\/deploy\/assets\/component-img-absolute\.svg\'\)/))
63+
/url\(\'\/assets\/component-img-absolute\.svg\'\)/))
6564
// Check urls with deploy-url and base-href scheme only use deploy-url.
6665
.then(() => ng('build', '--base-href=http://base.url/', '--deploy-url=http://deploy.url/',
6766
'--extract-css'))
6867
.then(() => expectFileToMatch('dist/test-project/styles.css',
69-
/url\(\'http:\/\/deploy\.url\/assets\/global-img-absolute\.svg\'\)/))
68+
/url\(\'\/assets\/global-img-absolute\.svg\'\)/))
7069
.then(() => expectFileToMatch('dist/test-project/main.js',
71-
/url\(\'http:\/\/deploy\.url\/assets\/component-img-absolute\.svg\'\)/))
70+
/url\(\'\/assets\/component-img-absolute\.svg\'\)/))
7271
// Check with base-href and deploy-url flags.
7372
.then(() => ng('build', '--base-href=/base/', '--deploy-url=deploy/',
7473
'--extract-css', '--aot'))
7574
.then(() => expectFileToMatch('dist/test-project/styles.css',
76-
'/base/deploy/assets/global-img-absolute.svg'))
75+
'/assets/global-img-absolute.svg'))
7776
.then(() => expectFileToMatch('dist/test-project/styles.css',
7877
/global-img-relative\.png/))
7978
.then(() => expectFileToMatch('dist/test-project/main.js',
80-
'/base/deploy/assets/component-img-absolute.svg'))
79+
'/assets/component-img-absolute.svg'))
8180
.then(() => expectFileToMatch('dist/test-project/main.js',
8281
/deploy\/component-img-relative\.png/))
8382
// Check with identical base-href and deploy-url flags.
8483
.then(() => ng('build', '--base-href=/base/', '--deploy-url=/base/',
8584
'--extract-css', '--aot'))
8685
.then(() => expectFileToMatch('dist/test-project/styles.css',
87-
'/base/assets/global-img-absolute.svg'))
86+
'/assets/global-img-absolute.svg'))
8887
.then(() => expectFileToMatch('dist/test-project/styles.css',
8988
/global-img-relative\.png/))
9089
.then(() => expectFileToMatch('dist/test-project/main.js',
91-
'/base/assets/component-img-absolute.svg'))
90+
'/assets/component-img-absolute.svg'))
9291
.then(() => expectFileToMatch('dist/test-project/main.js',
9392
/\/base\/component-img-relative\.png/))
9493
// Check with only base-href flag.
9594
.then(() => ng('build', '--base-href=/base/',
9695
'--extract-css', '--aot'))
9796
.then(() => expectFileToMatch('dist/test-project/styles.css',
98-
'/base/assets/global-img-absolute.svg'))
97+
'/assets/global-img-absolute.svg'))
9998
.then(() => expectFileToMatch('dist/test-project/styles.css',
10099
/global-img-relative\.png/))
101100
.then(() => expectFileToMatch('dist/test-project/main.js',
102-
'/base/assets/component-img-absolute.svg'))
101+
'/assets/component-img-absolute.svg'))
103102
.then(() => expectFileToMatch('dist/test-project/main.js',
104103
/component-img-relative\.png/));
105104
}

0 commit comments

Comments
 (0)