ng-packagr
Version:
Compile and package Angular libraries in Angular Package Format (APF)
304 lines • 14.5 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.LoadPathsUrlRebasingImporter = exports.ModuleUrlRebasingImporter = exports.RelativeUrlRebasingImporter = void 0;
exports.sassBindWorkaround = sassBindWorkaround;
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
const node_url_1 = require("node:url");
const lexer_1 = require("./lexer");
/**
* A Sass Importer base class that provides the load logic to rebase all `url()` functions
* within a stylesheet. The rebasing will ensure that the URLs in the output of the Sass compiler
* reflect the final filesystem location of the output CSS file.
*
* This class provides the core of the rebasing functionality. To ensure that each file is processed
* by this importer's load implementation, the Sass compiler requires the importer's canonicalize
* function to return a non-null value with the resolved location of the requested stylesheet.
* Concrete implementations of this class must provide this canonicalize functionality for rebasing
* to be effective.
*/
class UrlRebasingImporter {
/**
* @param entryDirectory The directory of the entry stylesheet that was passed to the Sass compiler.
* @param rebaseSourceMaps When provided, rebased files will have an intermediate sourcemap added to the Map
* which can be used to generate a final sourcemap that contains original sources.
*/
constructor(entryDirectory) {
this.entryDirectory = entryDirectory;
}
load(canonicalUrl) {
const stylesheetPath = (0, node_url_1.fileURLToPath)(canonicalUrl);
const stylesheetDirectory = (0, node_path_1.dirname)(stylesheetPath);
let contents = (0, node_fs_1.readFileSync)(stylesheetPath, 'utf-8');
// Rebase any URLs that are found
let updatedContents;
for (const { start, end, value } of (0, lexer_1.findUrls)(contents)) {
// Skip if value is empty or Webpack-specific prefix
if (value.length === 0 || value[0] === '~' || value[0] === '^') {
continue;
}
// Skip if root-relative, absolute or protocol relative url
if (/^((?:\w+:)?\/\/|data:|chrome:|\/)/.test(value)) {
continue;
}
// Skip if a fragment identifier but not a Sass interpolation
if (value[0] === '#' && value[1] !== '{') {
continue;
}
// Skip if value is value contains a function call
if (/#\{.+\(.+\)\}/.test(value)) {
continue;
}
// Sass variable usage either starts with a `$` or contains a namespace and a `.$`
const valueNormalized = value[0] === '$' || /^\w+\.\$/.test(value) ? `#{${value}}` : value;
const rebasedPath = (0, node_path_1.relative)(this.entryDirectory, stylesheetDirectory);
// Normalize path separators and escape characters
// https://developer.mozilla.org/en-US/docs/Web/CSS/url#syntax
const rebasedUrl = rebasedPath.replace(/\\/g, '/').replace(/[()\s'"]/g, '\\$&');
updatedContents !== null && updatedContents !== void 0 ? updatedContents : (updatedContents = contents);
updatedContents = contents.slice(0, start) + `"${rebasedUrl}||file:${valueNormalized}"` + contents.slice(end);
}
if (updatedContents) {
contents = updatedContents;
}
let syntax;
switch ((0, node_path_1.extname)(stylesheetPath).toLowerCase()) {
case '.css':
syntax = 'css';
break;
case '.sass':
syntax = 'indented';
break;
default:
syntax = 'scss';
break;
}
return {
contents,
syntax,
sourceMapUrl: canonicalUrl,
};
}
}
/**
* Provides the Sass importer logic to resolve relative stylesheet imports via both import and use rules
* and also rebase any `url()` function usage within those stylesheets. The rebasing will ensure that
* the URLs in the output of the Sass compiler reflect the final filesystem location of the output CSS file.
*/
class RelativeUrlRebasingImporter extends UrlRebasingImporter {
constructor(entryDirectory, directoryCache = new Map()) {
super(entryDirectory);
this.directoryCache = directoryCache;
}
canonicalize(url, options) {
return this.resolveImport(url, options.fromImport, true);
}
/**
* Attempts to resolve a provided URL to a stylesheet file using the Sass compiler's resolution algorithm.
* Based on https://github.com/sass/dart-sass/blob/44d6bb6ac72fe6b93f5bfec371a1fffb18e6b76d/lib/src/importer/utils.dart
* @param url The file protocol URL to resolve.
* @param fromImport If true, URL was from an import rule; otherwise from a use rule.
* @param checkDirectory If true, try checking for a directory with the base name containing an index file.
* @returns A full resolved URL of the stylesheet file or `null` if not found.
*/
resolveImport(url, fromImport, checkDirectory) {
var _a;
let stylesheetPath;
try {
stylesheetPath = (0, node_url_1.fileURLToPath)(url);
}
catch {
// Only file protocol URLs are supported by this importer
return null;
}
const directory = (0, node_path_1.dirname)(stylesheetPath);
const extension = (0, node_path_1.extname)(stylesheetPath);
const hasStyleExtension = extension === '.scss' || extension === '.sass' || extension === '.css';
// Remove the style extension if present to allow adding the `.import` suffix
const filename = (0, node_path_1.basename)(stylesheetPath, hasStyleExtension ? extension : undefined);
const importPotentials = new Set();
const defaultPotentials = new Set();
if (hasStyleExtension) {
if (fromImport) {
importPotentials.add(filename + '.import' + extension);
importPotentials.add('_' + filename + '.import' + extension);
}
defaultPotentials.add(filename + extension);
defaultPotentials.add('_' + filename + extension);
}
else {
if (fromImport) {
importPotentials.add(filename + '.import.scss');
importPotentials.add(filename + '.import.sass');
importPotentials.add(filename + '.import.css');
importPotentials.add('_' + filename + '.import.scss');
importPotentials.add('_' + filename + '.import.sass');
importPotentials.add('_' + filename + '.import.css');
}
defaultPotentials.add(filename + '.scss');
defaultPotentials.add(filename + '.sass');
defaultPotentials.add(filename + '.css');
defaultPotentials.add('_' + filename + '.scss');
defaultPotentials.add('_' + filename + '.sass');
defaultPotentials.add('_' + filename + '.css');
}
let foundDefaults;
let foundImports;
let hasPotentialIndex = false;
let cachedEntries = this.directoryCache.get(directory);
if (cachedEntries) {
// If there is a preprocessed cache of the directory, perform an intersection of the potentials
// and the directory files.
const { files, directories } = cachedEntries;
foundDefaults = [...defaultPotentials].filter(potential => files.has(potential));
foundImports = [...importPotentials].filter(potential => files.has(potential));
hasPotentialIndex = checkDirectory && !hasStyleExtension && directories.has(filename);
}
else {
// If no preprocessed cache exists, get the entries from the file system and, while searching,
// generate the cache for later requests.
let entries;
try {
entries = (0, node_fs_1.readdirSync)(directory, { withFileTypes: true });
}
catch (error) {
// If the containing directory does not exist return null to indicate it cannot be resolved
if (error.code === 'ENOENT') {
return null;
}
throw new Error(`Error reading directory ["${directory}"] while resolving Sass import`, {
cause: error,
});
}
foundDefaults = [];
foundImports = [];
cachedEntries = { files: new Set(), directories: new Set() };
for (const entry of entries) {
let isDirectory;
let isFile;
if (entry.isSymbolicLink()) {
const stats = (0, node_fs_1.statSync)((0, node_path_1.join)(directory, entry.name));
isDirectory = stats.isDirectory();
isFile = stats.isFile();
}
else {
isDirectory = entry.isDirectory();
isFile = entry.isFile();
}
if (isDirectory) {
cachedEntries.directories.add(entry.name);
// Record if the name should be checked as a directory with an index file
if (checkDirectory && !hasStyleExtension && entry.name === filename) {
hasPotentialIndex = true;
}
}
if (!isFile) {
continue;
}
cachedEntries.files.add(entry.name);
if (importPotentials.has(entry.name)) {
foundImports.push(entry.name);
}
if (defaultPotentials.has(entry.name)) {
foundDefaults.push(entry.name);
}
}
this.directoryCache.set(directory, cachedEntries);
}
// `foundImports` will only contain elements if `options.fromImport` is true
const result = (_a = this.checkFound(foundImports)) !== null && _a !== void 0 ? _a : this.checkFound(foundDefaults);
if (result !== null) {
return (0, node_url_1.pathToFileURL)((0, node_path_1.join)(directory, result));
}
if (hasPotentialIndex) {
// Check for index files using filename as a directory
return this.resolveImport(url + '/index', fromImport, false);
}
return null;
}
/**
* Checks an array of potential stylesheet files to determine if there is a valid
* stylesheet file. More than one discovered file may indicate an error.
* @param found An array of discovered stylesheet files.
* @returns A fully resolved path for a stylesheet file or `null` if not found.
* @throws If there are ambiguous files discovered.
*/
checkFound(found) {
if (found.length === 0) {
// Not found
return null;
}
// More than one found file may be an error
if (found.length > 1) {
// Presence of CSS files alongside a Sass file does not cause an error
const foundWithoutCss = found.filter(element => (0, node_path_1.extname)(element) !== '.css');
// If the length is zero then there are two or more css files
// If the length is more than one than there are two or more sass/scss files
if (foundWithoutCss.length !== 1) {
throw new Error('Ambiguous import detected.');
}
// Return the non-CSS file (sass/scss files have priority)
// https://github.com/sass/dart-sass/blob/44d6bb6ac72fe6b93f5bfec371a1fffb18e6b76d/lib/src/importer/utils.dart#L44-L47
return foundWithoutCss[0];
}
return found[0];
}
}
exports.RelativeUrlRebasingImporter = RelativeUrlRebasingImporter;
/**
* Provides the Sass importer logic to resolve module (npm package) stylesheet imports via both import and
* use rules and also rebase any `url()` function usage within those stylesheets. The rebasing will ensure that
* the URLs in the output of the Sass compiler reflect the final filesystem location of the output CSS file.
*/
class ModuleUrlRebasingImporter extends RelativeUrlRebasingImporter {
constructor(entryDirectory, directoryCache, finder) {
super(entryDirectory, directoryCache);
this.finder = finder;
}
canonicalize(url, options) {
if (url.startsWith('file://')) {
return super.canonicalize(url, options);
}
let result = this.finder(url, options);
result && (result = super.canonicalize(result.href, options));
return result;
}
}
exports.ModuleUrlRebasingImporter = ModuleUrlRebasingImporter;
/**
* Provides the Sass importer logic to resolve load paths located stylesheet imports via both import and
* use rules and also rebase any `url()` function usage within those stylesheets. The rebasing will ensure that
* the URLs in the output of the Sass compiler reflect the final filesystem location of the output CSS file.
*/
class LoadPathsUrlRebasingImporter extends RelativeUrlRebasingImporter {
constructor(entryDirectory, directoryCache, loadPaths) {
super(entryDirectory, directoryCache);
this.loadPaths = loadPaths;
}
canonicalize(url, options) {
if (url.startsWith('file://')) {
return super.canonicalize(url, options);
}
let result = null;
for (const loadPath of this.loadPaths) {
result = super.canonicalize((0, node_url_1.pathToFileURL)((0, node_path_1.join)(loadPath, url)).href, options);
if (result !== null) {
break;
}
}
return result;
}
}
exports.LoadPathsUrlRebasingImporter = LoadPathsUrlRebasingImporter;
/**
* Workaround for Sass not calling instance methods with `this`.
* The `canonicalize` and `load` methods will be bound to the class instance.
* @param importer A Sass importer to bind.
* @returns The bound Sass importer.
*/
function sassBindWorkaround(importer) {
importer.canonicalize = importer.canonicalize.bind(importer);
importer.load = importer.load.bind(importer);
return importer;
}
//# sourceMappingURL=rebasing-importer.js.map
;