Skip to content

Commit

Permalink
Merge pull request #241 from angular/main
Browse files Browse the repository at this point in the history
Create a new pull request by comparing changes across two branches
  • Loading branch information
GulajavaMinistudio authored Oct 10, 2024
2 parents 9157189 + 46a6324 commit e19ac75
Show file tree
Hide file tree
Showing 41 changed files with 1,001 additions and 486 deletions.
17 changes: 17 additions & 0 deletions adev/src/content/ecosystem/service-workers/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ export interface DataGroup {
maxSize: number;
maxAge: string;
timeout?: string;
refreshAhead?: string;
strategy?: 'freshness' | 'performance';
};
cacheQueryOptions?: {
Expand Down Expand Up @@ -270,6 +271,22 @@ The network timeout is how long the Angular service worker waits for the network

For example, the string `5s30u` translates to five seconds and 30 milliseconds of network timeout.


##### `refreshAhead`

This duration string specifies the time ahead of the expiration of a cached resource when the Angular service worker should proactively attempt to refresh the resource from the network.
The `refreshAhead` duration is an optional configuration that determines how much time before the expiration of a cached response the service worker should initiate a request to refresh the resource from the network.

| Suffixes | Details |
|:--- |:--- |
| `d` | Days |
| `h` | Hours |
| `m` | Minutes |
| `s` | Seconds |
| `u` | Milliseconds |

For example, the string `1h30m` translates to one hour and 30 minutes ahead of the expiration time.

##### `strategy`

The Angular service worker can use either of two caching strategies for data resources.
Expand Down
1 change: 1 addition & 0 deletions goldens/public-api/service-worker/config/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export interface DataGroup {
maxSize: number;
maxAge: Duration;
timeout?: Duration;
refreshAhead?: Duration;
strategy?: 'freshness' | 'performance';
cacheOpaqueResponses?: boolean;
};
Expand Down
170 changes: 140 additions & 30 deletions packages/compiler/src/shadow_css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ export class ShadowCss {
* captures how many (if any) leading whitespaces are present or a comma
* - (?:(?:(['"])((?:\\\\|\\\2|(?!\2).)+)\2)|(-?[A-Za-z][\w\-]*))
* captures two different possible keyframes, ones which are quoted or ones which are valid css
* idents (custom properties excluded)
* indents (custom properties excluded)
* - (?=[,\s;]|$)
* simply matches the end of the possible keyframe, valid endings are: a comma, a space, a
* semicolon or the end of the string
Expand Down Expand Up @@ -461,7 +461,7 @@ export class ShadowCss {
*/
private _scopeCssText(cssText: string, scopeSelector: string, hostSelector: string): string {
const unscopedRules = this._extractUnscopedRulesFromCssText(cssText);
// replace :host and :host-context -shadowcsshost and -shadowcsshost respectively
// replace :host and :host-context with -shadowcsshost and -shadowcsshostcontext respectively
cssText = this._insertPolyfillHostInCssText(cssText);
cssText = this._convertColonHost(cssText);
cssText = this._convertColonHostContext(cssText);
Expand Down Expand Up @@ -541,7 +541,7 @@ export class ShadowCss {
* .foo<scopeName> .bar { ... }
*/
private _convertColonHostContext(cssText: string): string {
return cssText.replace(_cssColonHostContextReGlobal, (selectorText) => {
return cssText.replace(_cssColonHostContextReGlobal, (selectorText, pseudoPrefix) => {
// We have captured a selector that contains a `:host-context` rule.

// For backward compatibility `:host-context` may contain a comma separated list of selectors.
Expand Down Expand Up @@ -596,10 +596,12 @@ export class ShadowCss {
}

// The context selectors now must be combined with each other to capture all the possible
// selectors that `:host-context` can match. See `combineHostContextSelectors()` for more
// selectors that `:host-context` can match. See `_combineHostContextSelectors()` for more
// info about how this is done.
return contextSelectorGroups
.map((contextSelectors) => combineHostContextSelectors(contextSelectors, selectorText))
.map((contextSelectors) =>
_combineHostContextSelectors(contextSelectors, selectorText, pseudoPrefix),
)
.join(', ');
});
}
Expand All @@ -618,7 +620,12 @@ export class ShadowCss {
let selector = rule.selector;
let content = rule.content;
if (rule.selector[0] !== '@') {
selector = this._scopeSelector(rule.selector, scopeSelector, hostSelector);
selector = this._scopeSelector({
selector,
scopeSelector,
hostSelector,
isParentSelector: true,
});
} else if (scopedAtRuleIdentifiers.some((atRule) => rule.selector.startsWith(atRule))) {
content = this._scopeSelectors(rule.content, scopeSelector, hostSelector);
} else if (rule.selector.startsWith('@font-face') || rule.selector.startsWith('@page')) {
Expand Down Expand Up @@ -658,15 +665,44 @@ export class ShadowCss {
});
}

private _scopeSelector(selector: string, scopeSelector: string, hostSelector: string): string {
private _safeSelector: SafeSelector | undefined;
private _shouldScopeIndicator: boolean | undefined;

// `isParentSelector` is used to distinguish the selectors which are coming from
// the initial selector string and any nested selectors, parsed recursively,
// for example `selector = 'a:where(.one)'` could be the parent, while recursive call
// would have `selector = '.one'`.
private _scopeSelector({
selector,
scopeSelector,
hostSelector,
isParentSelector = false,
}: {
selector: string;
scopeSelector: string;
hostSelector: string;
isParentSelector?: boolean;
}): string {
// Split the selector into independent parts by `,` (comma) unless
// comma is within parenthesis, for example `:is(.one, two)`.
// Negative lookup after comma allows not splitting inside nested parenthesis,
// up to three levels (((,))).
const selectorSplitRe =
/ ?,(?!(?:[^)(]*(?:\([^)(]*(?:\([^)(]*(?:\([^)(]*\)[^)(]*)*\)[^)(]*)*\)[^)(]*)*\))) ?/;

return selector
.split(/ ?, ?/)
.split(selectorSplitRe)
.map((part) => part.split(_shadowDeepSelectors))
.map((deepParts) => {
const [shallowPart, ...otherParts] = deepParts;
const applyScope = (shallowPart: string) => {
if (this._selectorNeedsScoping(shallowPart, scopeSelector)) {
return this._applySelectorScope(shallowPart, scopeSelector, hostSelector);
return this._applySelectorScope({
selector: shallowPart,
scopeSelector,
hostSelector,
isParentSelector,
});
} else {
return shallowPart;
}
Expand Down Expand Up @@ -699,9 +735,9 @@ export class ShadowCss {
if (_polyfillHostRe.test(selector)) {
const replaceBy = `[${hostSelector}]`;
return selector
.replace(_polyfillHostNoCombinatorRe, (hnc, selector) => {
.replace(_polyfillHostNoCombinatorReGlobal, (_hnc, selector) => {
return selector.replace(
/([^:]*)(:*)(.*)/,
/([^:\)]*)(:*)(.*)/,
(_: string, before: string, colon: string, after: string) => {
return before + replaceBy + colon + after;
},
Expand All @@ -715,11 +751,17 @@ export class ShadowCss {

// return a selector with [name] suffix on each simple selector
// e.g. .foo.bar > .zot becomes .foo[name].bar[name] > .zot[name] /** @internal */
private _applySelectorScope(
selector: string,
scopeSelector: string,
hostSelector: string,
): string {
private _applySelectorScope({
selector,
scopeSelector,
hostSelector,
isParentSelector,
}: {
selector: string;
scopeSelector: string;
hostSelector: string;
isParentSelector?: boolean;
}): string {
const isRe = /\[is=([^\]]*)\]/g;
scopeSelector = scopeSelector.replace(isRe, (_: string, ...parts: string[]) => parts[0]);

Expand All @@ -734,6 +776,10 @@ export class ShadowCss {

if (p.includes(_polyfillHostNoCombinator)) {
scopedP = this._applySimpleSelectorScope(p, scopeSelector, hostSelector);
if (_polyfillHostNoCombinatorWithinPseudoFunction.test(p)) {
const [_, before, colon, after] = scopedP.match(/([^:]*)(:*)(.*)/)!;
scopedP = before + attrName + colon + after;
}
} else {
// remove :host since it should be unnecessary
const t = p.replace(_polyfillHostRe, '');
Expand All @@ -748,13 +794,60 @@ export class ShadowCss {
return scopedP;
};

const safeContent = new SafeSelector(selector);
selector = safeContent.content();
// Wraps `_scopeSelectorPart()` to not use it directly on selectors with
// pseudo selector functions like `:where()`. Selectors within pseudo selector
// functions are recursively sent to `_scopeSelector()`.
const _pseudoFunctionAwareScopeSelectorPart = (selectorPart: string) => {
let scopedPart = '';

const cssPrefixWithPseudoSelectorFunctionMatch = selectorPart.match(
_cssPrefixWithPseudoSelectorFunction,
);
if (cssPrefixWithPseudoSelectorFunctionMatch) {
const [cssPseudoSelectorFunction] = cssPrefixWithPseudoSelectorFunctionMatch;

// Unwrap the pseudo selector to scope its contents.
// For example,
// - `:where(selectorToScope)` -> `selectorToScope`;
// - `:is(.foo, .bar)` -> `.foo, .bar`.
const selectorToScope = selectorPart.slice(cssPseudoSelectorFunction.length, -1);

if (selectorToScope.includes(_polyfillHostNoCombinator)) {
this._shouldScopeIndicator = true;
}

const scopedInnerPart = this._scopeSelector({
selector: selectorToScope,
scopeSelector,
hostSelector,
});

// Put the result back into the pseudo selector function.
scopedPart = `${cssPseudoSelectorFunction}${scopedInnerPart})`;
} else {
this._shouldScopeIndicator =
this._shouldScopeIndicator || selectorPart.includes(_polyfillHostNoCombinator);
scopedPart = this._shouldScopeIndicator ? _scopeSelectorPart(selectorPart) : selectorPart;
}

return scopedPart;
};

if (isParentSelector) {
this._safeSelector = new SafeSelector(selector);
selector = this._safeSelector.content();
}

let scopedSelector = '';
let startIndex = 0;
let res: RegExpExecArray | null;
const sep = /( |>|\+|~(?!=))\s*/g;
// Combinators aren't used as a delimiter if they are within parenthesis,
// for example `:where(.one .two)` stays intact.
// Similarly to selector separation by comma initially, negative lookahead
// is used here to not break selectors within nested parenthesis up to three
// nested layers.
const sep =
/( |>|\+|~(?!=))(?!([^)(]*(?:\([^)(]*(?:\([^)(]*(?:\([^)(]*\)[^)(]*)*\)[^)(]*)*\)[^)(]*)*\)))\s*/g;

// If a selector appears before :host it should not be shimmed as it
// matches on ancestor elements and not on elements in the host's shadow
Expand All @@ -768,8 +861,13 @@ export class ShadowCss {
// - `tag :host` -> `tag [h]` (`tag` is not scoped because it's considered part of a
// `:host-context(tag)`)
const hasHost = selector.includes(_polyfillHostNoCombinator);
// Only scope parts after the first `-shadowcsshost-no-combinator` when it is present
let shouldScope = !hasHost;
// Only scope parts after or on the same level as the first `-shadowcsshost-no-combinator`
// when it is present. The selector has the same level when it is a part of a pseudo
// selector, like `:where()`, for example `:where(:host, .foo)` would result in `.foo`
// being scoped.
if (isParentSelector || this._shouldScopeIndicator) {
this._shouldScopeIndicator = !hasHost;
}

while ((res = sep.exec(selector)) !== null) {
const separator = res[1];
Expand All @@ -788,18 +886,17 @@ export class ShadowCss {
continue;
}

shouldScope = shouldScope || part.includes(_polyfillHostNoCombinator);
const scopedPart = shouldScope ? _scopeSelectorPart(part) : part;
const scopedPart = _pseudoFunctionAwareScopeSelectorPart(part);
scopedSelector += `${scopedPart} ${separator} `;
startIndex = sep.lastIndex;
}

const part = selector.substring(startIndex);
shouldScope = shouldScope || part.includes(_polyfillHostNoCombinator);
scopedSelector += shouldScope ? _scopeSelectorPart(part) : part;
scopedSelector += _pseudoFunctionAwareScopeSelectorPart(part);

// replace the placeholders with their original values
return safeContent.restore(scopedSelector);
// using values stored inside the `safeSelector` instance.
return this._safeSelector!.restore(scopedSelector);
}

private _insertPolyfillHostInCssText(selector: string): string {
Expand Down Expand Up @@ -864,6 +961,8 @@ class SafeSelector {
}
}

const _cssScopedPseudoFunctionPrefix = '(:(where|is)\\()?';
const _cssPrefixWithPseudoSelectorFunction = /^:(where|is)\(/i;
const _cssContentNextSelectorRe =
/polyfill-next-selector[^}]*content:[\s]*?(['"])(.*?)\1[;\s]*}([^{]*?){/gim;
const _cssContentRuleRe = /(polyfill-rule)[^}]*(content:[\s]*(['"])(.*?)\3)[;\s]*[^}]*}/gim;
Expand All @@ -874,10 +973,17 @@ const _polyfillHost = '-shadowcsshost';
const _polyfillHostContext = '-shadowcsscontext';
const _parenSuffix = '(?:\\((' + '(?:\\([^)(]*\\)|[^)(]*)+?' + ')\\))?([^,{]*)';
const _cssColonHostRe = new RegExp(_polyfillHost + _parenSuffix, 'gim');
const _cssColonHostContextReGlobal = new RegExp(_polyfillHostContext + _parenSuffix, 'gim');
const _cssColonHostContextReGlobal = new RegExp(
_cssScopedPseudoFunctionPrefix + '(' + _polyfillHostContext + _parenSuffix + ')',
'gim',
);
const _cssColonHostContextRe = new RegExp(_polyfillHostContext + _parenSuffix, 'im');
const _polyfillHostNoCombinator = _polyfillHost + '-no-combinator';
const _polyfillHostNoCombinatorWithinPseudoFunction = new RegExp(
`:.*\\(.*${_polyfillHostNoCombinator}.*\\)`,
);
const _polyfillHostNoCombinatorRe = /-shadowcsshost-no-combinator([^\s]*)/;
const _polyfillHostNoCombinatorReGlobal = new RegExp(_polyfillHostNoCombinatorRe, 'g');
const _shadowDOMSelectorsRe = [
/::shadow/g,
/::content/g,
Expand Down Expand Up @@ -1128,7 +1234,11 @@ function unescapeQuotes(str: string, isQuoted: boolean): string {
* @param contextSelectors an array of context selectors that will be combined.
* @param otherSelectors the rest of the selectors that are not context selectors.
*/
function combineHostContextSelectors(contextSelectors: string[], otherSelectors: string): string {
function _combineHostContextSelectors(
contextSelectors: string[],
otherSelectors: string,
pseudoPrefix = '',
): string {
const hostMarker = _polyfillHostNoCombinator;
_polyfillHostRe.lastIndex = 0; // reset the regex to ensure we get an accurate test
const otherSelectorsHasHost = _polyfillHostRe.test(otherSelectors);
Expand Down Expand Up @@ -1157,8 +1267,8 @@ function combineHostContextSelectors(contextSelectors: string[], otherSelectors:
return combined
.map((s) =>
otherSelectorsHasHost
? `${s}${otherSelectors}`
: `${s}${hostMarker}${otherSelectors}, ${s} ${hostMarker}${otherSelectors}`,
? `${pseudoPrefix}${s}${otherSelectors}`
: `${pseudoPrefix}${s}${hostMarker}${otherSelectors}, ${pseudoPrefix}${s} ${hostMarker}${otherSelectors}`,
)
.join(',');
}
Expand Down
36 changes: 36 additions & 0 deletions packages/compiler/test/shadow_css/host_and_host_context_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,42 @@ describe('ShadowCss, :host and :host-context', () => {
});

describe(':host-context', () => {
it('should transform :host-context with pseudo selectors', () => {
expect(
shim(':host-context(backdrop:not(.borderless)) .backdrop {}', 'contenta', 'hosta'),
).toEqualCss(
'backdrop:not(.borderless)[hosta] .backdrop[contenta], backdrop:not(.borderless) [hosta] .backdrop[contenta] {}',
);
expect(shim(':where(:host-context(backdrop)) {}', 'contenta', 'hosta')).toEqualCss(
':where(backdrop[hosta]), :where(backdrop [hosta]) {}',
);
expect(shim(':where(:host-context(outer1)) :host(bar) {}', 'contenta', 'hosta')).toEqualCss(
':where(outer1) bar[hosta] {}',
);
expect(
shim(':where(:host-context(.one)) :where(:host-context(.two)) {}', 'contenta', 'a-host'),
).toEqualCss(
':where(.one.two[a-host]), ' + // `one` and `two` both on the host
':where(.one.two [a-host]), ' + // `one` and `two` are both on the same ancestor
':where(.one .two[a-host]), ' + // `one` is an ancestor and `two` is on the host
':where(.one .two [a-host]), ' + // `one` and `two` are both ancestors (in that order)
':where(.two .one[a-host]), ' + // `two` is an ancestor and `one` is on the host
':where(.two .one [a-host])' + // `two` and `one` are both ancestors (in that order)
' {}',
);
expect(
shim(':where(:host-context(backdrop)) .foo ~ .bar {}', 'contenta', 'hosta'),
).toEqualCss(
':where(backdrop[hosta]) .foo[contenta] ~ .bar[contenta], :where(backdrop [hosta]) .foo[contenta] ~ .bar[contenta] {}',
);
expect(shim(':where(:host-context(backdrop)) :host {}', 'contenta', 'hosta')).toEqualCss(
':where(backdrop) [hosta] {}',
);
expect(shim('div:where(:host-context(backdrop)) :host {}', 'contenta', 'hosta')).toEqualCss(
'div:where(backdrop) [hosta] {}',
);
});

it('should handle tag selector', () => {
expect(shim(':host-context(div) {}', 'contenta', 'a-host')).toEqualCss(
'div[a-host], div [a-host] {}',
Expand Down
Loading

0 comments on commit e19ac75

Please sign in to comment.