diff options
Diffstat (limited to 'src/compiler/transform/styles.ts')
-rw-r--r-- | src/compiler/transform/styles.ts | 290 |
1 files changed, 0 insertions, 290 deletions
diff --git a/src/compiler/transform/styles.ts b/src/compiler/transform/styles.ts deleted file mode 100644 index 53585651f..000000000 --- a/src/compiler/transform/styles.ts +++ /dev/null @@ -1,290 +0,0 @@ -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<any>; - } -} - -const getStyleType: Map<string, StyleType> = 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<string, string>; // 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<string, string>(), -}; - -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<StyleTransformResult> { - 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: <style lang="${styleType}">`); - } - } - - // 2. Post-process (PostCSS) - const postcssPlugins: Plugin[] = []; - - // 2a. Tailwind (only if project uses Tailwind) - if (miniCache.tailwindEnabled) { - try { - const require = createRequire(import.meta.url); - const tw = require.resolve('tailwindcss', { paths: [import.meta.url, process.cwd()] }); - postcssPlugins.push(require(tw) as any); - } catch (err) { - // eslint-disable-next-line no-console - console.error(err); - throw new Error(`tailwindcss not installed. Try running \`npm install tailwindcss\` and trying again.`); - } - } - - // 2b. Astro scoped styles (always on) - postcssPlugins.push(astroScopedStyles({ className: scopedClass })); - - // 2c. Scoped @keyframes - postcssPlugins.push( - postcssKeyframes({ - generateScopedName(keyframesName) { - return `${keyframesName}-${scopedClass}`; - }, - }) - ); - - // 2d. Autoprefixer (always on) - postcssPlugins.push(autoprefixer()); - - // 2e. Run PostCSS - css = await postcss(postcssPlugins) - .process(css, { from: filename, to: undefined }) - .then((result) => result.css); - - return { css, type: styleType }; -} - -/** Transform <style> tags */ -export default function transformStyles({ compileOptions, filename, fileID }: TransformOptions): Transformer { - const styleNodes: TemplateNode[] = []; // <style> tags to be updated - const styleTransformPromises: Promise<StyleTransformResult>[] = []; // async style transform results to be finished in finalize(); - const scopedClass = `astro-${hashFromFilename(fileID)}`; // this *should* generate same hash from fileID every time - - // find Tailwind config, if first run (cache for subsequent runs) - if (miniCache.tailwindEnabled === undefined) { - const tailwindNames = ['tailwind.config.js', 'tailwind.config.mjs']; - for (const loc of tailwindNames) { - const tailwindLoc = path.join(fileURLToPath(compileOptions.astroConfig.projectRoot), loc); - if (fs.existsSync(tailwindLoc)) { - miniCache.tailwindEnabled = true; // Success! We have a Tailwind config file. - debug(compileOptions.logging, 'tailwind', 'Found config. Enabling.'); - break; - } - } - if (miniCache.tailwindEnabled !== true) miniCache.tailwindEnabled = false; // We couldn‘t find one; mark as false - debug(compileOptions.logging, 'tailwind', 'No config found. Skipping.'); - } - - return { - visitors: { - html: { - Element: { - enter(node) { - // 1. if <style> tag, transform it and continue to next node - if (node.name === 'style') { - // Same as ast.css (below) - const code = Array.isArray(node.children) ? node.children.map(({ data }: any) => data).join('\n') : ''; - if (!code) return; - const langAttr = (node.attributes || []).find(({ name }: any) => name === 'lang'); - styleNodes.push(node); - styleTransformPromises.push( - transformStyle(code, { - type: (langAttr && langAttr.value[0] && langAttr.value[0].data) || undefined, - filename, - scopedClass, - mode: compileOptions.mode, - }) - ); - return; - } - - // 2. add scoped HTML classes - if (NEVER_SCOPED_TAGS.has(node.name)) return; // only continue if this is NOT a <script> tag, etc. - // Note: currently we _do_ scope web components/custom elements. This seems correct? - - if (!node.attributes) node.attributes = []; - const classIndex = node.attributes.findIndex(({ name }: any) => name === 'class'); - if (classIndex === -1) { - // 3a. element has no class="" attribute; add one and append scopedClass - node.attributes.push({ start: -1, end: -1, type: 'Attribute', name: 'class', value: [{ type: 'Text', raw: scopedClass, data: scopedClass }] }); - } else { - // 3b. element has class=""; append scopedClass - const attr = node.attributes[classIndex]; - for (let k = 0; k < attr.value.length; k++) { - if (attr.value[k].type === 'Text') { - // don‘t add same scopedClass twice - if (!hasClass(attr.value[k].data, scopedClass)) { - // string literal - attr.value[k].raw += ' ' + scopedClass; - attr.value[k].data += ' ' + scopedClass; - } - } else if (attr.value[k].type === 'MustacheTag' && attr.value[k]) { - // don‘t add same scopedClass twice (this check is a little more basic, but should suffice) - if (!attr.value[k].expression.codeChunks[0].includes(`' ${scopedClass}'`)) { - // MustacheTag - // FIXME: this won't work when JSX element can appear in attributes (rare but possible). - attr.value[k].expression.codeChunks[0] = `(${attr.value[k].expression.codeChunks[0]}) + ' ${scopedClass}'`; - } - } - } - } - }, - }, - }, - // CSS: compile styles, apply CSS Modules scoping - css: { - Style: { - enter(node) { - // Same as ast.html (above) - // Note: this is duplicated from html because of the compiler we‘re using; in a future version we should combine these - if (!node.content || !node.content.styles) return; - const code = node.content.styles; - const langAttr = (node.attributes || []).find(({ name }: any) => name === 'lang'); - styleNodes.push(node); - styleTransformPromises.push( - transformStyle(code, { - type: (langAttr && langAttr.value[0] && langAttr.value[0].data) || undefined, - filename, - scopedClass, - mode: compileOptions.mode, - }) - ); - }, - }, - }, - }, - async finalize() { - const styleTransforms = await Promise.all(styleTransformPromises); - - styleTransforms.forEach((result, n) => { - if (styleNodes[n].attributes) { - // 1. Replace with final CSS - const isHeadStyle = !styleNodes[n].content; - if (isHeadStyle) { - // Note: <style> tags in <head> have different attributes/rules, because of the parser. Unknown why - (styleNodes[n].children as any) = [{ ...(styleNodes[n].children as any)[0], data: result.css }]; - } else { - styleNodes[n].content.styles = result.css; - } - - // 2. Update <style> attributes - const styleTypeIndex = styleNodes[n].attributes.findIndex(({ name }: any) => name === 'type'); - // add type="text/css" - if (styleTypeIndex !== -1) { - styleNodes[n].attributes[styleTypeIndex].value[0].raw = 'text/css'; - styleNodes[n].attributes[styleTypeIndex].value[0].data = 'text/css'; - } else { - styleNodes[n].attributes.push({ name: 'type', type: 'Attribute', value: [{ type: 'Text', raw: 'text/css', data: 'text/css' }] }); - } - // remove lang="*" - const styleLangIndex = styleNodes[n].attributes.findIndex(({ name }: any) => name === 'lang'); - if (styleLangIndex !== -1) styleNodes[n].attributes.splice(styleLangIndex, 1); - // TODO: add data-astro for later - // styleNodes[n].attributes.push({ name: 'data-astro', type: 'Attribute', value: true }); - } - }); - }, - }; -} |