diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/options.ts b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/options.ts index 7f4f68bc4a4a..3012039d2a16 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/options.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/options.ts @@ -13,6 +13,7 @@ import path from 'node:path'; import { normalizeAssetPatterns, normalizeOptimization, normalizeSourceMaps } from '../../utils'; import { normalizeCacheOptions } from '../../utils/normalize-cache'; import { generateEntryPoints } from '../../utils/package-chunk-sort'; +import { findTailwindConfigurationFile } from '../../utils/tailwind'; import { getIndexInputFile, getIndexOutputFile } from '../../utils/webpack-browser-config'; import { globalScriptsByBundleName, normalizeGlobalStyles } from '../../webpack/utils/helpers'; import { Schema as BrowserBuilderOptions, OutputHashing } from './schema'; @@ -97,7 +98,7 @@ export async function normalizeOptions( } let tailwindConfiguration: { file: string; package: string } | undefined; - const tailwindConfigurationPath = findTailwindConfigurationFile(workspaceRoot, projectRoot); + const tailwindConfigurationPath = await findTailwindConfigurationFile(workspaceRoot, projectRoot); if (tailwindConfigurationPath) { // Create a node resolver at the project root as a directory const resolver = createRequire(projectRoot + '/'); @@ -203,27 +204,6 @@ export async function normalizeOptions( }; } -function findTailwindConfigurationFile( - workspaceRoot: string, - projectRoot: string, -): string | undefined { - // A configuration file can exist in the project or workspace root - // The list of valid config files can be found: - // https://github.com/tailwindlabs/tailwindcss/blob/8845d112fb62d79815b50b3bae80c317450b8b92/src/util/resolveConfigPath.js#L46-L52 - const tailwindConfigFiles = ['tailwind.config.js', 'tailwind.config.cjs']; - for (const basePath of [projectRoot, workspaceRoot]) { - for (const configFile of tailwindConfigFiles) { - // Project level configuration should always take precedence. - const fullPath = path.join(basePath, configFile); - if (fs.existsSync(fullPath)) { - return fullPath; - } - } - } - - return undefined; -} - /** * Normalize a directory path string. * Currently only removes a trailing slash if present. diff --git a/packages/angular_devkit/build_angular/src/utils/tailwind.ts b/packages/angular_devkit/build_angular/src/utils/tailwind.ts new file mode 100644 index 000000000000..fea5f9d8bb96 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/utils/tailwind.ts @@ -0,0 +1,40 @@ +/** + * @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 + */ + +import { readdir } from 'node:fs/promises'; +import { join } from 'node:path'; + +const tailwindConfigFiles: string[] = [ + 'tailwind.config.js', + 'tailwind.config.cjs', + 'tailwind.config.mjs', + 'tailwind.config.ts', +]; + +export async function findTailwindConfigurationFile( + workspaceRoot: string, + projectRoot: string, +): Promise { + const dirEntries = [projectRoot, workspaceRoot].map((root) => + readdir(root, { withFileTypes: false }).then((entries) => ({ + root, + files: new Set(entries), + })), + ); + + // A configuration file can exist in the project or workspace root + for await (const { root, files } of dirEntries) { + for (const potentialConfig of tailwindConfigFiles) { + if (files.has(potentialConfig)) { + return join(root, potentialConfig); + } + } + } + + return undefined; +} diff --git a/packages/angular_devkit/build_angular/src/webpack/configs/styles.ts b/packages/angular_devkit/build_angular/src/webpack/configs/styles.ts index eac8f9ab8d54..33e1b825d41e 100644 --- a/packages/angular_devkit/build_angular/src/webpack/configs/styles.ts +++ b/packages/angular_devkit/build_angular/src/webpack/configs/styles.ts @@ -7,7 +7,6 @@ */ import MiniCssExtractPlugin from 'mini-css-extract-plugin'; -import * as fs from 'node:fs'; import * as path from 'node:path'; import { pathToFileURL } from 'node:url'; import type { FileImporter } from 'sass'; @@ -19,6 +18,7 @@ import { import { SassLegacyWorkerImplementation } from '../../sass/sass-service-legacy'; import { WebpackConfigOptions } from '../../utils/build-options'; import { useLegacySass } from '../../utils/environment-options'; +import { findTailwindConfigurationFile } from '../../utils/tailwind'; import { AnyComponentStyleBudgetChecker, PostcssCliResources, @@ -34,8 +34,8 @@ import { } from '../utils/helpers'; // eslint-disable-next-line max-lines-per-function -export function getStylesConfig(wco: WebpackConfigOptions): Configuration { - const { root, buildOptions, logger } = wco; +export async function getStylesConfig(wco: WebpackConfigOptions): Promise { + const { root, buildOptions, logger, projectRoot } = wco; const extraPlugins: Configuration['plugins'] = []; extraPlugins.push(new AnyComponentStyleBudgetChecker(buildOptions.budgets)); @@ -86,13 +86,13 @@ export function getStylesConfig(wco: WebpackConfigOptions): Configuration { // Only load Tailwind CSS plugin if configuration file was found. // This acts as a guard to ensure the project actually wants to use Tailwind CSS. // The package may be unknowningly present due to a third-party transitive package dependency. - const tailwindConfigPath = getTailwindConfigPath(wco); + const tailwindConfigPath = await findTailwindConfigurationFile(root, projectRoot); if (tailwindConfigPath) { let tailwindPackagePath; try { - tailwindPackagePath = require.resolve('tailwindcss', { paths: [wco.root] }); + tailwindPackagePath = require.resolve('tailwindcss', { paths: [root] }); } catch { - const relativeTailwindConfigPath = path.relative(wco.root, tailwindConfigPath); + const relativeTailwindConfigPath = path.relative(root, tailwindConfigPath); logger.warn( `Tailwind CSS configuration file found (${relativeTailwindConfigPath})` + ` but the 'tailwindcss' package is not installed.` + @@ -315,24 +315,6 @@ export function getStylesConfig(wco: WebpackConfigOptions): Configuration { }; } -function getTailwindConfigPath({ projectRoot, root }: WebpackConfigOptions): string | undefined { - // A configuration file can exist in the project or workspace root - // The list of valid config files can be found: - // https://github.com/tailwindlabs/tailwindcss/blob/8845d112fb62d79815b50b3bae80c317450b8b92/src/util/resolveConfigPath.js#L46-L52 - const tailwindConfigFiles = ['tailwind.config.js', 'tailwind.config.cjs']; - for (const basePath of [projectRoot, root]) { - for (const configFile of tailwindConfigFiles) { - // Irrespective of the name project level configuration should always take precedence. - const fullPath = path.join(basePath, configFile); - if (fs.existsSync(fullPath)) { - return fullPath; - } - } - } - - return undefined; -} - function getSassLoaderOptions( root: string, implementation: SassWorkerImplementation | SassLegacyWorkerImplementation, @@ -399,7 +381,7 @@ function getSassResolutionImporter( root: string, preserveSymlinks: boolean, ): FileImporter<'async'> { - const commonResolverOptions: Parameters[0] = { + const commonResolverOptions: Parameters<(typeof loaderContext)['getResolve']>[0] = { conditionNames: ['sass', 'style'], mainFields: ['sass', 'style', 'main', '...'], extensions: ['.scss', '.sass', '.css'],