Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions src/cdk/schematics/ng-update/devkit-migration-rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,9 @@ export function createMigrationSchematicRule(
}

// In some applications, developers will have global stylesheets which are not
// specified in any Angular component. Therefore we glob up all CSS and SCSS files
// in the project and migrate them if needed.
// TODO: rework this to collect global stylesheets from the workspace config.
// TODO: https://github.com/angular/components/issues/24032.
const additionalStylesheetPaths = findStylesheetFiles(tree, project.root);
// specified in any Angular component. We consult the workspace config for each
// project and migrate only the explicitly configured global styles.
const additionalStylesheetPaths = findStylesheetFiles(tree, project);

if (buildTsconfigPath !== null) {
runMigrations(project, projectName, buildTsconfigPath, additionalStylesheetPaths, false);
Expand Down
117 changes: 109 additions & 8 deletions src/cdk/schematics/ng-update/find-stylesheets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,111 @@
* found in the LICENSE file at https://angular.dev/license
*/

import {join, Path} from '@angular-devkit/core';
import {join, normalize, Path} from '@angular-devkit/core';
import {Tree} from '@angular-devkit/schematics';
import {ProjectDefinition} from '@schematics/angular/utility';

/** Regular expression that matches stylesheet paths */
const STYLESHEET_REGEX = /.*\.(css|scss)$/;

/**
* Finds stylesheets in the given directory from within the specified tree.
* @param tree Devkit tree where stylesheet files can be found in.
* @param startDirectory Optional start directory where stylesheets should be searched in.
* This can be useful if only stylesheets within a given folder are relevant (to avoid
* unnecessary iterations).
* Finds stylesheets referenced by the workspace configuration. Falls back to scanning a
* directory tree when a specific project definition is not provided.
*
* @param tree Virtual file system tree used by schematics.
* @param projectOrDirectory Project definition to inspect or a path to scan recursively.
*/
export function findStylesheetFiles(
tree: Tree,
projectOrDirectory: ProjectDefinition | string = '/',
): string[] {
if (isProjectDefinition(projectOrDirectory)) {
return findFromWorkspaceProject(tree, projectOrDirectory);
}

return findByTraversal(tree, projectOrDirectory);
}

/**
* Collects all styles declared inside the workspace configuration for a given project.
*
* @param tree Virtual file system tree used for file existence checks.
* @param project Angular workspace project whose styles should be gathered.
*/
function findFromWorkspaceProject(tree: Tree, project: ProjectDefinition): string[] {
const styles = new Set<string>();

project.targets?.forEach(target => {
collectFromTarget(target?.options, tree, styles);

if (target?.configurations) {
Object.values(target.configurations).forEach(config =>
collectFromTarget(config, tree, styles),
);
}
});

return Array.from(styles);
}

/**
* Extracts stylesheet entries from a target/options object and adds them to the accumulator.
*
* @param options Architect options object that may contain a `styles` array.
* @param tree Virtual file system tree used to validate style paths.
* @param styles Accumulator set that stores normalized stylesheet paths.
*/
function collectFromTarget(
options: Record<string, any> | undefined,
tree: Tree,
styles: Set<string>,
) {
const optionStyles = options?.['styles'];

if (!Array.isArray(optionStyles)) {
return;
}

optionStyles.forEach(entry => {
const stylesheetPath = coerceStylesheetPath(entry);

if (!stylesheetPath || !STYLESHEET_REGEX.test(stylesheetPath)) {
return;
}

const normalizedPath = normalize(stylesheetPath);

if (tree.exists(normalizedPath)) {
styles.add(normalizedPath);
}
});
}

/**
* Normalizes a `styles` entry into a plain path string.
*
* @param value Value from the `styles` array (string or object with `input`).
*/
export function findStylesheetFiles(tree: Tree, startDirectory: string = '/'): string[] {
function coerceStylesheetPath(value: unknown): string | null {
if (typeof value === 'string') {
return value;
}

if (value && typeof value === 'object') {
const input = (value as Record<string, unknown>)['input'];
return typeof input === 'string' ? input : null;
}

return null;
}

/**
* Traverses directories starting from the provided path and gathers stylesheet files.
*
* @param tree Virtual file system tree used for directory traversal.
* @param startDirectory Directory path whose subtree should be searched.
*/
function findByTraversal(tree: Tree, startDirectory: string): string[] {
const result: string[] = [];
const visitDir = (dirPath: Path) => {
const {subfiles, subdirs} = tree.getDir(dirPath);
Expand All @@ -38,6 +129,16 @@ export function findStylesheetFiles(tree: Tree, startDirectory: string = '/'): s
}
});
};
visitDir(startDirectory as Path);

visitDir((startDirectory || '/') as Path);
return result;
}

/**
* Type guard that checks whether the provided value is a `ProjectDefinition`.
*
* @param value Unknown value to test.
*/
function isProjectDefinition(value: unknown): value is ProjectDefinition {
return !!value && typeof value === 'object' && 'targets' in (value as ProjectDefinition);
}