mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(core): support styles and styleUrl as strings (#51715)
Adds support for passing in `@Component.styles` as a string. Also introduces a new `styleUrl` property on `@Component` for providing a single stylesheet. This is more convenient for the most common case where a component only has one stylesheet associated with it. PR Close #51715
This commit is contained in:
committed by
Andrew Kushnir
parent
f9939757d3
commit
59387ee476
@@ -452,7 +452,8 @@ export function transformDecoratorResources(
|
||||
|
||||
// If no external resources are referenced, preserve the original decorator
|
||||
// for the best source map experience when the decorator is emitted in TS.
|
||||
if (!component.has('templateUrl') && !component.has('styleUrls') && !component.has('styles')) {
|
||||
if (!component.has('templateUrl') && !component.has('styleUrls') && !component.has('styleUrl') &&
|
||||
!component.has('styles')) {
|
||||
return dec;
|
||||
}
|
||||
|
||||
@@ -464,9 +465,10 @@ export function transformDecoratorResources(
|
||||
metadata.set('template', ts.factory.createStringLiteral(template.content));
|
||||
}
|
||||
|
||||
if (metadata.has('styleUrls') || metadata.has('styles')) {
|
||||
if (metadata.has('styleUrls') || metadata.has('styleUrl') || metadata.has('styles')) {
|
||||
metadata.delete('styles');
|
||||
metadata.delete('styleUrls');
|
||||
metadata.delete('styleUrl');
|
||||
|
||||
if (styles.length > 0) {
|
||||
const styleNodes = styles.reduce((result, style) => {
|
||||
@@ -496,11 +498,35 @@ export function extractComponentStyleUrls(
|
||||
evaluator: PartialEvaluator,
|
||||
component: Map<string, ts.Expression>,
|
||||
): StyleUrlMeta[] {
|
||||
if (!component.has('styleUrls')) {
|
||||
return [];
|
||||
const styleUrlsExpr = component.get('styleUrls');
|
||||
const styleUrlExpr = component.get('styleUrl');
|
||||
|
||||
if (styleUrlsExpr !== undefined && styleUrlExpr !== undefined) {
|
||||
throw new FatalDiagnosticError(
|
||||
ErrorCode.COMPONENT_INVALID_STYLE_URLS, styleUrlExpr,
|
||||
'@Component cannot define both `styleUrl` and `styleUrls`. ' +
|
||||
'Use `styleUrl` if the component has one stylesheet, or `styleUrls` if it has multiple');
|
||||
}
|
||||
|
||||
return extractStyleUrlsFromExpression(evaluator, component.get('styleUrls')!);
|
||||
if (styleUrlsExpr !== undefined) {
|
||||
return extractStyleUrlsFromExpression(evaluator, component.get('styleUrls')!);
|
||||
}
|
||||
|
||||
if (styleUrlExpr !== undefined) {
|
||||
const styleUrl = evaluator.evaluate(styleUrlExpr);
|
||||
|
||||
if (typeof styleUrl !== 'string') {
|
||||
throw createValueHasWrongTypeError(styleUrlExpr, styleUrl, 'styleUrl must be a string');
|
||||
}
|
||||
|
||||
return [{
|
||||
url: styleUrl,
|
||||
source: ResourceTypeForDiagnostics.StylesheetFromDecorator,
|
||||
nodeForError: styleUrlExpr,
|
||||
}];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function extractStyleUrlsFromExpression(
|
||||
@@ -543,42 +569,63 @@ function extractStyleUrlsFromExpression(
|
||||
|
||||
return styleUrls;
|
||||
}
|
||||
|
||||
export function extractStyleResources(
|
||||
resourceLoader: ResourceLoader, component: Map<string, ts.Expression>,
|
||||
containingFile: string): ReadonlySet<Resource> {
|
||||
const styles = new Set<Resource>();
|
||||
function stringLiteralElements(array: ts.ArrayLiteralExpression): ts.StringLiteralLike[] {
|
||||
return array.elements.filter(
|
||||
(e: ts.Expression): e is ts.StringLiteralLike => ts.isStringLiteralLike(e));
|
||||
return array.elements.filter((e): e is ts.StringLiteralLike => ts.isStringLiteralLike(e));
|
||||
}
|
||||
|
||||
// If styleUrls is a literal array, process each resource url individually and
|
||||
// register ones that are string literals.
|
||||
// If styleUrls is a literal array, process each resource url individually and register ones that
|
||||
// are string literals. If `styleUrl` is specified, register a single stylesheet. Note that
|
||||
// `styleUrl` and `styleUrls` are mutually-exclusive. This is validated in
|
||||
// `extractComponentStyleUrls`.
|
||||
const styleUrlExpr = component.get('styleUrl');
|
||||
const styleUrlsExpr = component.get('styleUrls');
|
||||
if (styleUrlsExpr !== undefined && ts.isArrayLiteralExpression(styleUrlsExpr)) {
|
||||
for (const expression of stringLiteralElements(styleUrlsExpr)) {
|
||||
try {
|
||||
const resourceUrl = resourceLoader.resolve(expression.text, containingFile);
|
||||
styles.add({path: absoluteFrom(resourceUrl), expression});
|
||||
} catch {
|
||||
// Errors in style resource extraction do not need to be handled here. We will produce
|
||||
// diagnostics for each one that fails in the analysis, after we evaluate the
|
||||
// `styleUrls` expression to determine _all_ style resources, not just the string
|
||||
// literals.
|
||||
const resource = stringLiteralUrlToResource(resourceLoader, expression, containingFile);
|
||||
if (resource !== null) {
|
||||
styles.add(resource);
|
||||
}
|
||||
}
|
||||
} else if (styleUrlExpr !== undefined && ts.isStringLiteralLike(styleUrlExpr)) {
|
||||
const resource = stringLiteralUrlToResource(resourceLoader, styleUrlExpr, containingFile);
|
||||
if (resource !== null) {
|
||||
styles.add(resource);
|
||||
}
|
||||
}
|
||||
|
||||
const stylesExpr = component.get('styles');
|
||||
if (stylesExpr !== undefined && ts.isArrayLiteralExpression(stylesExpr)) {
|
||||
for (const expression of stringLiteralElements(stylesExpr)) {
|
||||
styles.add({path: null, expression});
|
||||
if (stylesExpr !== undefined) {
|
||||
if (ts.isArrayLiteralExpression(stylesExpr)) {
|
||||
for (const expression of stringLiteralElements(stylesExpr)) {
|
||||
styles.add({path: null, expression});
|
||||
}
|
||||
} else if (ts.isStringLiteralLike(stylesExpr)) {
|
||||
styles.add({path: null, expression: stylesExpr});
|
||||
}
|
||||
}
|
||||
|
||||
return styles;
|
||||
}
|
||||
|
||||
function stringLiteralUrlToResource(
|
||||
resourceLoader: ResourceLoader, expression: ts.StringLiteralLike,
|
||||
containingFile: string): Resource|null {
|
||||
try {
|
||||
const resourceUrl = resourceLoader.resolve(expression.text, containingFile);
|
||||
return {path: absoluteFrom(resourceUrl), expression};
|
||||
} catch {
|
||||
// Errors in style resource extraction do not need to be handled here. We will produce
|
||||
// diagnostics for each one that fails in the analysis, after we evaluate the `styleUrls`
|
||||
// expression to determine _all_ style resources, not just the string literals.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function _extractTemplateStyleUrls(template: ParsedTemplateWithSource): StyleUrlMeta[] {
|
||||
if (template.styleUrls === null) {
|
||||
return [];
|
||||
|
||||
@@ -470,7 +470,8 @@ export function parseDirectiveStyles(
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = evaluator.evaluate(expression);
|
||||
const evaluated = evaluator.evaluate(expression);
|
||||
const value = typeof evaluated === 'string' ? [evaluated] : evaluated;
|
||||
|
||||
// Create specific error if any string is imported from external file in local compilation mode
|
||||
if (compilationMode === CompilationMode.LOCAL && Array.isArray(value)) {
|
||||
@@ -481,7 +482,7 @@ export function parseDirectiveStyles(
|
||||
const chain: ts.DiagnosticMessageChain = {
|
||||
messageText: `Unknown identifier used as styles string: ${
|
||||
entry.node
|
||||
.getText()} (did you import this string from another file? This is not allowed in local compilation mode. Please either inline it or move it to a separate file and include it using'styleUrls')`,
|
||||
.getText()} (did you import this string from another file? This is not allowed in local compilation mode. Please either inline it or move it to a separate file and include it using 'styleUrl')`,
|
||||
category: ts.DiagnosticCategory.Error,
|
||||
code: 0,
|
||||
};
|
||||
@@ -495,7 +496,8 @@ export function parseDirectiveStyles(
|
||||
|
||||
if (!isStringArrayOrDie(value, 'styles', expression)) {
|
||||
throw createValueHasWrongTypeError(
|
||||
expression, value, `Failed to resolve @Directive.styles to a string array`);
|
||||
expression, value,
|
||||
`Failed to resolve @Component.styles to a string or an array of strings`);
|
||||
}
|
||||
|
||||
return value;
|
||||
|
||||
@@ -113,6 +113,8 @@ export enum ErrorCode {
|
||||
*/
|
||||
CONFLICTING_INPUT_TRANSFORM = 2020,
|
||||
|
||||
/** Raised when a component has both `styleUrls` and `styleUrl`. */
|
||||
COMPONENT_INVALID_STYLE_URLS = 2021,
|
||||
|
||||
SYMBOL_NOT_EXPORTED = 3001,
|
||||
/**
|
||||
|
||||
@@ -7436,6 +7436,61 @@ function allTests(os: string) {
|
||||
// Large string should be called from function definition.
|
||||
'_c0()]');
|
||||
});
|
||||
|
||||
it('should process `styles` as a string', () => {
|
||||
env.write('test.ts', `
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: '',
|
||||
styles: 'h2 {width: 10px}'
|
||||
})
|
||||
export class TestCmp {}
|
||||
`);
|
||||
|
||||
env.driveMain();
|
||||
const jsContents = env.getContents('test.js');
|
||||
expect(jsContents).toContain('styles: ["h2[_ngcontent-%COMP%] {width: 10px}"]');
|
||||
});
|
||||
|
||||
it('should process `styleUrl`', () => {
|
||||
env.write('dir/styles.css', 'h2 {width: 10px}');
|
||||
env.write('test.ts', `
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'test-cmp',
|
||||
styleUrl: 'dir/styles.css',
|
||||
template: '',
|
||||
})
|
||||
export class TestCmp {}
|
||||
`);
|
||||
env.driveMain();
|
||||
|
||||
const jsContents = env.getContents('test.js');
|
||||
expect(jsContents).not.toContain('styleUrl');
|
||||
expect(jsContents).toContain('styles: ["h2[_ngcontent-%COMP%] {width: 10px}"]');
|
||||
});
|
||||
|
||||
it('should produce a diagnostic if both `styleUrls` and `styleUrl` are defined', () => {
|
||||
env.write('dir/styles.css', 'h2 {width: 10px}');
|
||||
env.write('test.ts', `
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'test-cmp',
|
||||
styleUrl: 'dir/styles.css',
|
||||
styleUrls: ['dir/styles.css'],
|
||||
template: '',
|
||||
})
|
||||
export class TestCmp {}
|
||||
`);
|
||||
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags.length).toBe(1);
|
||||
expect(diags[0].messageText)
|
||||
.toContain('@Component cannot define both `styleUrl` and `styleUrls`');
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty resources', () => {
|
||||
|
||||
Reference in New Issue
Block a user