import crypto from 'crypto'; import fs from 'fs'; import { createRequire } from 'module'; import path from 'path'; import { fileURLToPath } from 'url'; import autoprefixer from 'autoprefixer'; import postcss, { Plugin } from 'postcss'; import postcssKeyframes from 'postcss-icss-keyframes'; import findUp from 'find-up'; import sass from 'sass'; import type { RuntimeMode } from '../../@types/astro'; import type { TransformOptions, Transformer } from '../../@types/transformer'; import type { TemplateNode } from '../../parser/interfaces'; import { debug } from '../../logger.js'; import astroScopedStyles, { NEVER_SCOPED_TAGS } from './postcss-scoped-styles/index.js'; type StyleType = 'css' | 'scss' | 'sass' | 'postcss'; declare global { interface ImportMeta { /** https://nodejs.org/api/esm.html#esm_import_meta_resolve_specifier_parent */ resolve(specifier: string, parent?: string): Promise; } } const getStyleType: Map = new Map([ ['.css', 'css'], ['.pcss', 'postcss'], ['.sass', 'sass'], ['.scss', 'scss'], ['css', 'css'], ['sass', 'sass'], ['scss', 'scss'], ['text/css', 'css'], ['text/sass', 'sass'], ['text/scss', 'scss'], ]); /** Should be deterministic, given a unique filename */ function hashFromFilename(filename: string): string { const hash = crypto.createHash('sha256'); return hash .update(filename.replace(/\\/g, '/')) .digest('base64') .toString() .replace(/[^A-Za-z0-9-]/g, '') .substr(0, 8); } export interface StyleTransformResult { css: string; type: StyleType; } interface StylesMiniCache { nodeModules: Map; // filename: node_modules location tailwindEnabled?: boolean; // cache once per-run } /** Simple cache that only exists in memory per-run. Prevents the same lookups from happening over and over again within the same build or dev server session. */ const miniCache: StylesMiniCache = { nodeModules: new Map(), }; export interface TransformStyleOptions { type?: string; filename: string; scopedClass: string; mode: RuntimeMode; } /** given a class="" string, does it contain a given class? */ function hasClass(classList: string, className: string): boolean { if (!className) return false; for (const c of classList.split(' ')) { if (className === c.trim()) return true; } return false; } /** Convert styles to scoped CSS */ async function transformStyle(code: string, { type, filename, scopedClass, mode }: TransformStyleOptions): Promise { let styleType: StyleType = 'css'; // important: assume CSS as default if (type) { styleType = getStyleType.get(type) || styleType; } // add file path to includePaths let includePaths: string[] = [path.dirname(filename)]; // include node_modules to includePaths (allows @use-ing node modules, if it can be located) const cachedNodeModulesDir = miniCache.nodeModules.get(filename); if (cachedNodeModulesDir) { includePaths.push(cachedNodeModulesDir); } else { const nodeModulesDir = await findUp('node_modules', { type: 'directory', cwd: path.dirname(filename) }); if (nodeModulesDir) { miniCache.nodeModules.set(filename, nodeModulesDir); includePaths.push(nodeModulesDir); } } // 1. Preprocess (currently only Sass supported) let css = ''; switch (styleType) { case 'css': { css = code; break; } case 'sass': case 'scss': { css = sass.renderSync({ data: code, includePaths }).css.toString('utf8'); break; } default: { throw new Error(`Unsupported: