-
Notifications
You must be signed in to change notification settings - Fork 12k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
perf(@angular-devkit/build-angular): inject Sass import/use directive…
… importer information when resolving To correctly resolve a package based import reference in a Sass file with pnpm or Yarn PnP, the importer file path must be known. Unfortunately, the Sass compiler does not provided the importer file to import plugins. Previously to workaround this issue, all previously resolved stylesheets were tried as the importer path. This allowed the stylesheets to be resolved but it also could cause a potentially large increase in build time due to the amount of previous stylesheets that would need to be tried. To avoid the performance impact and to also provide more accurate information regarding the importer file, a lexer is now used to extract import information for a stylesheet and inject the importer file path into the specifier. This information is then extracted from the import specifier during the Sass resolution process and allows the underlying package resolution access to a viable location to resolve the package for all package managers. This information is currently limited to specifiers referencing the `@angular` and `@material` package scopes but a comprehensive pre-resolution process may be added in the future.
- Loading branch information
Showing
4 changed files
with
355 additions
and
196 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
267 changes: 267 additions & 0 deletions
267
packages/angular_devkit/build_angular/src/tools/sass/lexer.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,267 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
// TODO: Combine everything into a single pass lexer | ||
|
||
/** | ||
* Determines if a unicode code point is a CSS whitespace character. | ||
* @param code The unicode code point to test. | ||
* @returns true, if the code point is CSS whitespace; false, otherwise. | ||
*/ | ||
function isWhitespace(code: number): boolean { | ||
// Based on https://www.w3.org/TR/css-syntax-3/#whitespace | ||
switch (code) { | ||
case 0x0009: // tab | ||
case 0x0020: // space | ||
case 0x000a: // line feed | ||
case 0x000c: // form feed | ||
case 0x000d: // carriage return | ||
return true; | ||
default: | ||
return false; | ||
} | ||
} | ||
|
||
/** | ||
* Scans a CSS or Sass file and locates all valid url function values as defined by the | ||
* syntax specification. | ||
* @param contents A string containing a CSS or Sass file to scan. | ||
* @returns An iterable that yields each CSS url function value found. | ||
*/ | ||
export function* findUrls( | ||
contents: string, | ||
): Iterable<{ start: number; end: number; value: string }> { | ||
let pos = 0; | ||
let width = 1; | ||
let current = -1; | ||
const next = () => { | ||
pos += width; | ||
current = contents.codePointAt(pos) ?? -1; | ||
width = current > 0xffff ? 2 : 1; | ||
|
||
return current; | ||
}; | ||
|
||
// Based on https://www.w3.org/TR/css-syntax-3/#consume-ident-like-token | ||
while ((pos = contents.indexOf('url(', pos)) !== -1) { | ||
// Set to position of the ( | ||
pos += 3; | ||
width = 1; | ||
|
||
// Consume all leading whitespace | ||
while (isWhitespace(next())) { | ||
/* empty */ | ||
} | ||
|
||
// Initialize URL state | ||
const url = { start: pos, end: -1, value: '' }; | ||
let complete = false; | ||
|
||
// If " or ', then consume the value as a string | ||
if (current === 0x0022 || current === 0x0027) { | ||
const ending = current; | ||
// Based on https://www.w3.org/TR/css-syntax-3/#consume-string-token | ||
while (!complete) { | ||
switch (next()) { | ||
case -1: // EOF | ||
return; | ||
case 0x000a: // line feed | ||
case 0x000c: // form feed | ||
case 0x000d: // carriage return | ||
// Invalid | ||
complete = true; | ||
break; | ||
case 0x005c: // \ -- character escape | ||
// If not EOF or newline, add the character after the escape | ||
switch (next()) { | ||
case -1: | ||
return; | ||
case 0x000a: // line feed | ||
case 0x000c: // form feed | ||
case 0x000d: // carriage return | ||
// Skip when inside a string | ||
break; | ||
default: | ||
// TODO: Handle hex escape codes | ||
url.value += String.fromCodePoint(current); | ||
break; | ||
} | ||
break; | ||
case ending: | ||
// Full string position should include the quotes for replacement | ||
url.end = pos + 1; | ||
complete = true; | ||
yield url; | ||
break; | ||
default: | ||
url.value += String.fromCodePoint(current); | ||
break; | ||
} | ||
} | ||
|
||
next(); | ||
continue; | ||
} | ||
|
||
// Based on https://www.w3.org/TR/css-syntax-3/#consume-url-token | ||
while (!complete) { | ||
switch (current) { | ||
case -1: // EOF | ||
return; | ||
case 0x0022: // " | ||
case 0x0027: // ' | ||
case 0x0028: // ( | ||
// Invalid | ||
complete = true; | ||
break; | ||
case 0x0029: // ) | ||
// URL is valid and complete | ||
url.end = pos; | ||
complete = true; | ||
break; | ||
case 0x005c: // \ -- character escape | ||
// If not EOF or newline, add the character after the escape | ||
switch (next()) { | ||
case -1: // EOF | ||
return; | ||
case 0x000a: // line feed | ||
case 0x000c: // form feed | ||
case 0x000d: // carriage return | ||
// Invalid | ||
complete = true; | ||
break; | ||
default: | ||
// TODO: Handle hex escape codes | ||
url.value += String.fromCodePoint(current); | ||
break; | ||
} | ||
break; | ||
default: | ||
if (isWhitespace(current)) { | ||
while (isWhitespace(next())) { | ||
/* empty */ | ||
} | ||
// Unescaped whitespace is only valid before the closing ) | ||
if (current === 0x0029) { | ||
// URL is valid | ||
url.end = pos; | ||
} | ||
complete = true; | ||
} else { | ||
// Add the character to the url value | ||
url.value += String.fromCodePoint(current); | ||
} | ||
break; | ||
} | ||
next(); | ||
} | ||
|
||
// An end position indicates a URL was found | ||
if (url.end !== -1) { | ||
yield url; | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Scans a CSS or Sass file and locates all valid import/use directive values as defined by the | ||
* syntax specification. | ||
* @param contents A string containing a CSS or Sass file to scan. | ||
* @returns An iterable that yields each CSS directive value found. | ||
*/ | ||
export function* findImports( | ||
contents: string, | ||
): Iterable<{ start: number; end: number; specifier: string }> { | ||
yield* find(contents, '@import '); | ||
yield* find(contents, '@use '); | ||
} | ||
|
||
/** | ||
* Scans a CSS or Sass file and locates all valid function/directive values as defined by the | ||
* syntax specification. | ||
* @param contents A string containing a CSS or Sass file to scan. | ||
* @param prefix The prefix to start a valid segment. | ||
* @returns An iterable that yields each CSS url function value found. | ||
*/ | ||
function* find( | ||
contents: string, | ||
prefix: string, | ||
): Iterable<{ start: number; end: number; specifier: string }> { | ||
let pos = 0; | ||
let width = 1; | ||
let current = -1; | ||
const next = () => { | ||
pos += width; | ||
current = contents.codePointAt(pos) ?? -1; | ||
width = current > 0xffff ? 2 : 1; | ||
|
||
return current; | ||
}; | ||
|
||
// Based on https://www.w3.org/TR/css-syntax-3/#consume-ident-like-token | ||
while ((pos = contents.indexOf(prefix, pos)) !== -1) { | ||
// Set to position of the last character in prefix | ||
pos += prefix.length - 1; | ||
width = 1; | ||
|
||
// Consume all leading whitespace | ||
while (isWhitespace(next())) { | ||
/* empty */ | ||
} | ||
|
||
// Initialize URL state | ||
const url = { start: pos, end: -1, specifier: '' }; | ||
let complete = false; | ||
|
||
// If " or ', then consume the value as a string | ||
if (current === 0x0022 || current === 0x0027) { | ||
const ending = current; | ||
// Based on https://www.w3.org/TR/css-syntax-3/#consume-string-token | ||
while (!complete) { | ||
switch (next()) { | ||
case -1: // EOF | ||
return; | ||
case 0x000a: // line feed | ||
case 0x000c: // form feed | ||
case 0x000d: // carriage return | ||
// Invalid | ||
complete = true; | ||
break; | ||
case 0x005c: // \ -- character escape | ||
// If not EOF or newline, add the character after the escape | ||
switch (next()) { | ||
case -1: | ||
return; | ||
case 0x000a: // line feed | ||
case 0x000c: // form feed | ||
case 0x000d: // carriage return | ||
// Skip when inside a string | ||
break; | ||
default: | ||
// TODO: Handle hex escape codes | ||
url.specifier += String.fromCodePoint(current); | ||
break; | ||
} | ||
break; | ||
case ending: | ||
// Full string position should include the quotes for replacement | ||
url.end = pos + 1; | ||
complete = true; | ||
yield url; | ||
break; | ||
default: | ||
url.specifier += String.fromCodePoint(current); | ||
break; | ||
} | ||
} | ||
|
||
next(); | ||
continue; | ||
} | ||
} | ||
} |
Oops, something went wrong.