summaryrefslogtreecommitdiff
path: root/src/compiler/transform
diff options
context:
space:
mode:
authorGravatar Matthew Phillips <matthew@matthewphillips.info> 2021-04-09 14:09:13 -0400
committerGravatar GitHub <noreply@github.com> 2021-04-09 14:09:13 -0400
commitad9c3b1d8dbf1c3aff75497271347ed36ea38a0b (patch)
tree8e0aed5ea1783df8322e1db589e84f9579152ba3 /src/compiler/transform
parent084845f79d064626d5f5069ce7b945e3b44bdbd7 (diff)
downloadastro-ad9c3b1d8dbf1c3aff75497271347ed36ea38a0b.tar.gz
astro-ad9c3b1d8dbf1c3aff75497271347ed36ea38a0b.tar.zst
astro-ad9c3b1d8dbf1c3aff75497271347ed36ea38a0b.zip
Parse inner JSX as Astro (#67)
* Parse inner JSX as Astro This completes the compiler changes, updating the parser so that it parses inner "JSX" as Astro. It does this by finding the start and end of HTML tags and feeds that back into the parser. The result is a structure like this: ``` { type: 'MustacheTag', expression: [ { type: 'Expression', codeStart: 'colors.map(color => (', codeEnd: '}}' children: [ { type: 'Fragment', children: [ { type: 'Element', name: 'div' } ] } ] } ] } ``` There is a new Node type, `Expression`. Note that `MustacheTag` remains in the tree, all it contains is an Expression though. I could spend some time trying to remove it, there's just a few places that expect it to exist. * Update import to the transform * Transform prism components into expressions
Diffstat (limited to 'src/compiler/transform')
-rw-r--r--src/compiler/transform/doctype.ts35
-rw-r--r--src/compiler/transform/index.ts98
-rw-r--r--src/compiler/transform/module-scripts.ts43
-rw-r--r--src/compiler/transform/postcss-scoped-styles/index.ts106
-rw-r--r--src/compiler/transform/prism.ts90
-rw-r--r--src/compiler/transform/styles.ts285
6 files changed, 657 insertions, 0 deletions
diff --git a/src/compiler/transform/doctype.ts b/src/compiler/transform/doctype.ts
new file mode 100644
index 000000000..d19b01f81
--- /dev/null
+++ b/src/compiler/transform/doctype.ts
@@ -0,0 +1,35 @@
+import { Transformer } from '../../@types/transformer';
+
+/** Transform <!doctype> tg */
+export default function (_opts: { filename: string; fileID: string }): Transformer {
+ let hasDoctype = false;
+
+ return {
+ visitors: {
+ html: {
+ Element: {
+ enter(node, parent, _key, index) {
+ if (node.name === '!doctype') {
+ hasDoctype = true;
+ }
+ if (node.name === 'html' && !hasDoctype) {
+ const dtNode = {
+ start: 0,
+ end: 0,
+ attributes: [{ type: 'Attribute', name: 'html', value: true, start: 0, end: 0 }],
+ children: [],
+ name: '!doctype',
+ type: 'Element',
+ };
+ parent.children!.splice(index, 0, dtNode);
+ hasDoctype = true;
+ }
+ },
+ },
+ },
+ },
+ async finalize() {
+ // Nothing happening here.
+ },
+ };
+}
diff --git a/src/compiler/transform/index.ts b/src/compiler/transform/index.ts
new file mode 100644
index 000000000..6a81b92b0
--- /dev/null
+++ b/src/compiler/transform/index.ts
@@ -0,0 +1,98 @@
+import type { Ast, TemplateNode } from '../../parser/interfaces';
+import type { NodeVisitor, TransformOptions, Transformer, VisitorFn } from '../../@types/transformer';
+
+import { walk } from 'estree-walker';
+
+// Transformers
+import transformStyles from './styles.js';
+import transformDoctype from './doctype.js';
+import transformModuleScripts from './module-scripts.js';
+import transformCodeBlocks from './prism.js';
+
+interface VisitorCollection {
+ enter: Map<string, VisitorFn[]>;
+ leave: Map<string, VisitorFn[]>;
+}
+
+/** Add visitors to given collection */
+function addVisitor(visitor: NodeVisitor, collection: VisitorCollection, nodeName: string, event: 'enter' | 'leave') {
+ if (typeof visitor[event] !== 'function') return;
+ if (!collection[event]) collection[event] = new Map<string, VisitorFn[]>();
+
+ const visitors = collection[event].get(nodeName) || [];
+ visitors.push(visitor[event] as any);
+ collection[event].set(nodeName, visitors);
+}
+
+/** Compile visitor actions from transformer */
+function collectVisitors(transformer: Transformer, htmlVisitors: VisitorCollection, cssVisitors: VisitorCollection, finalizers: Array<() => Promise<void>>) {
+ if (transformer.visitors) {
+ if (transformer.visitors.html) {
+ for (const [nodeName, visitor] of Object.entries(transformer.visitors.html)) {
+ addVisitor(visitor, htmlVisitors, nodeName, 'enter');
+ addVisitor(visitor, htmlVisitors, nodeName, 'leave');
+ }
+ }
+ if (transformer.visitors.css) {
+ for (const [nodeName, visitor] of Object.entries(transformer.visitors.css)) {
+ addVisitor(visitor, cssVisitors, nodeName, 'enter');
+ addVisitor(visitor, cssVisitors, nodeName, 'leave');
+ }
+ }
+ }
+ finalizers.push(transformer.finalize);
+}
+
+/** Utility for formatting visitors */
+function createVisitorCollection() {
+ return {
+ enter: new Map<string, VisitorFn[]>(),
+ leave: new Map<string, VisitorFn[]>(),
+ };
+}
+
+/** Walk AST with collected visitors */
+function walkAstWithVisitors(tmpl: TemplateNode, collection: VisitorCollection) {
+ walk(tmpl, {
+ enter(node, parent, key, index) {
+ if (collection.enter.has(node.type)) {
+ const fns = collection.enter.get(node.type)!;
+ for (let fn of fns) {
+ fn.call(this, node, parent, key, index);
+ }
+ }
+ },
+ leave(node, parent, key, index) {
+ if (collection.leave.has(node.type)) {
+ const fns = collection.leave.get(node.type)!;
+ for (let fn of fns) {
+ fn.call(this, node, parent, key, index);
+ }
+ }
+ },
+ });
+}
+
+/**
+ * Transform
+ * Step 2/3 in Astro SSR.
+ * Transform is the point at which we mutate the AST before sending off to
+ * Codegen, and then to Snowpack. In some ways, it‘s a preprocessor.
+ */
+export async function transform(ast: Ast, opts: TransformOptions) {
+ const htmlVisitors = createVisitorCollection();
+ const cssVisitors = createVisitorCollection();
+ const finalizers: Array<() => Promise<void>> = [];
+
+ const optimizers = [transformStyles(opts), transformDoctype(opts), transformModuleScripts(opts), transformCodeBlocks(ast.module)];
+
+ for (const optimizer of optimizers) {
+ collectVisitors(optimizer, htmlVisitors, cssVisitors, finalizers);
+ }
+
+ walkAstWithVisitors(ast.css, cssVisitors);
+ walkAstWithVisitors(ast.html, htmlVisitors);
+
+ // Run all of the finalizer functions in parallel because why not.
+ await Promise.all(finalizers.map((fn) => fn()));
+}
diff --git a/src/compiler/transform/module-scripts.ts b/src/compiler/transform/module-scripts.ts
new file mode 100644
index 000000000..aff1ec4f6
--- /dev/null
+++ b/src/compiler/transform/module-scripts.ts
@@ -0,0 +1,43 @@
+import type { Transformer } from '../../@types/transformer';
+import type { CompileOptions } from '../../@types/compiler';
+
+import path from 'path';
+import { getAttrValue, setAttrValue } from '../../ast.js';
+
+/** Transform <script type="module"> */
+export default function ({ compileOptions, filename }: { compileOptions: CompileOptions; filename: string; fileID: string }): Transformer {
+ const { astroConfig } = compileOptions;
+ const { astroRoot } = astroConfig;
+ const fileUrl = new URL(`file://${filename}`);
+
+ return {
+ visitors: {
+ html: {
+ Element: {
+ enter(node) {
+ let name = node.name;
+ if (name !== 'script') {
+ return;
+ }
+
+ let type = getAttrValue(node.attributes, 'type');
+ if (type !== 'module') {
+ return;
+ }
+
+ let src = getAttrValue(node.attributes, 'src');
+ if (!src || !src.startsWith('.')) {
+ return;
+ }
+
+ const srcUrl = new URL(src, fileUrl);
+ const fromAstroRoot = path.posix.relative(astroRoot.pathname, srcUrl.pathname);
+ const absoluteUrl = `/_astro/${fromAstroRoot}`;
+ setAttrValue(node.attributes, 'src', absoluteUrl);
+ },
+ },
+ },
+ },
+ async finalize() {},
+ };
+}
diff --git a/src/compiler/transform/postcss-scoped-styles/index.ts b/src/compiler/transform/postcss-scoped-styles/index.ts
new file mode 100644
index 000000000..23350869c
--- /dev/null
+++ b/src/compiler/transform/postcss-scoped-styles/index.ts
@@ -0,0 +1,106 @@
+import { Declaration, Plugin } from 'postcss';
+
+interface AstroScopedOptions {
+ className: string;
+}
+
+interface Selector {
+ start: number;
+ end: number;
+ value: string;
+}
+
+const CSS_SEPARATORS = new Set([' ', ',', '+', '>', '~']);
+const KEYFRAME_PERCENT = /\d+\.?\d*%/;
+
+/** HTML tags that should never get scoped classes */
+export const NEVER_SCOPED_TAGS = new Set<string>(['base', 'body', 'font', 'frame', 'frameset', 'head', 'html', 'link', 'meta', 'noframes', 'noscript', 'script', 'style', 'title']);
+
+/**
+ * Scope Rules
+ * Given a selector string (`.btn>span,.nav>span`), add an additional CSS class to every selector (`.btn.myClass>span.myClass,.nav.myClass>span.myClass`)
+ * @param {string} selector The minified selector string to parse. Cannot contain arbitrary whitespace (other than child selector syntax).
+ * @param {string} className The CSS class to apply.
+ */
+export function scopeRule(selector: string, className: string) {
+ // if this is a keyframe keyword, return original selector
+ if (selector === 'from' || selector === 'to' || KEYFRAME_PERCENT.test(selector)) {
+ return selector;
+ }
+
+ // For everything else, parse & scope
+ const c = className.replace(/^\.?/, '.'); // make sure class always has leading '.'
+ const selectors: Selector[] = [];
+ let ss = selector; // final output
+
+ // Pass 1: parse selector string; extract top-level selectors
+ {
+ let start = 0;
+ let lastValue = '';
+ let parensOpen = false;
+ for (let n = 0; n < ss.length; n++) {
+ const isEnd = n === selector.length - 1;
+ if (selector[n] === '(') parensOpen = true;
+ if (selector[n] === ')') parensOpen = false;
+ if (isEnd || (parensOpen === false && CSS_SEPARATORS.has(selector[n]))) {
+ lastValue = selector.substring(start, isEnd ? undefined : n);
+ if (!lastValue) continue;
+ selectors.push({ start, end: isEnd ? n + 1 : n, value: lastValue });
+ start = n + 1;
+ }
+ }
+ }
+
+ // Pass 2: starting from end, transform selectors w/ scoped class
+ for (let i = selectors.length - 1; i >= 0; i--) {
+ const { start, end, value } = selectors[i];
+ const head = ss.substring(0, start);
+ const tail = ss.substring(end);
+
+ // replace '*' with className
+ if (value === '*') {
+ ss = head + c + tail;
+ continue;
+ }
+
+ // leave :global() alone!
+ if (value.startsWith(':global(')) {
+ ss =
+ head +
+ ss
+ .substring(start, end)
+ .replace(/^:global\(/, '')
+ .replace(/\)$/, '') +
+ tail;
+ continue;
+ }
+
+ // don‘t scope body, title, etc.
+ if (NEVER_SCOPED_TAGS.has(value)) {
+ ss = head + value + tail;
+ continue;
+ }
+
+ // scope everything else
+ let newSelector = ss.substring(start, end);
+ const pseudoIndex = newSelector.indexOf(':');
+ if (pseudoIndex > 0) {
+ // if there‘s a pseudoclass (:focus)
+ ss = head + newSelector.substring(start, pseudoIndex) + c + newSelector.substr(pseudoIndex) + tail;
+ } else {
+ ss = head + newSelector + c + tail;
+ }
+ }
+
+ return ss;
+}
+
+/** PostCSS Scope plugin */
+export default function astroScopedStyles(options: AstroScopedOptions): Plugin {
+ return {
+ postcssPlugin: '@astro/postcss-scoped-styles',
+ Rule(rule) {
+ rule.selector = scopeRule(rule.selector, options.className);
+ },
+ };
+}
diff --git a/src/compiler/transform/prism.ts b/src/compiler/transform/prism.ts
new file mode 100644
index 000000000..628dcce7e
--- /dev/null
+++ b/src/compiler/transform/prism.ts
@@ -0,0 +1,90 @@
+import type { Transformer } from '../../@types/transformer';
+import type { Script } from '../../parser/interfaces';
+import { getAttrValue } from '../../ast.js';
+
+const PRISM_IMPORT = `import Prism from 'astro/components/Prism.astro';\n`;
+const prismImportExp = /import Prism from ['"]astro\/components\/Prism.astro['"]/;
+
+function escape(code: string) {
+ return code.replace(/[`$]/g, (match) => {
+ return '\\' + match;
+ });
+}
+
+export default function (module: Script): Transformer {
+ let usesPrism = false;
+
+ return {
+ visitors: {
+ html: {
+ Element: {
+ enter(node) {
+ if (node.name !== 'code') return;
+ const className = getAttrValue(node.attributes, 'class') || '';
+ const classes = className.split(' ');
+
+ let lang;
+ for (let cn of classes) {
+ const matches = /language-(.+)/.exec(cn);
+ if (matches) {
+ lang = matches[1];
+ }
+ }
+
+ if (!lang) return;
+
+ let code;
+ if (node.children?.length) {
+ code = node.children[0].data;
+ }
+
+ const repl = {
+ start: 0,
+ end: 0,
+ type: 'InlineComponent',
+ name: 'Prism',
+ attributes: [
+ {
+ type: 'Attribute',
+ name: 'lang',
+ value: [
+ {
+ type: 'Text',
+ raw: lang,
+ data: lang,
+ },
+ ],
+ },
+ {
+ type: 'Attribute',
+ name: 'code',
+ value: [
+ {
+ type: 'MustacheTag',
+ expression: {
+ type: 'Expression',
+ codeStart: '`' + escape(code) + '`',
+ codeEnd: '',
+ children: []
+ }
+ },
+ ],
+ },
+ ],
+ children: [],
+ };
+
+ this.replace(repl);
+ usesPrism = true;
+ },
+ },
+ },
+ },
+ async finalize() {
+ // Add the Prism import if needed.
+ if (usesPrism && !prismImportExp.test(module.content)) {
+ module.content = PRISM_IMPORT + module.content;
+ }
+ },
+ };
+}
diff --git a/src/compiler/transform/styles.ts b/src/compiler/transform/styles.ts
new file mode 100644
index 000000000..d8e3196a1
--- /dev/null
+++ b/src/compiler/transform/styles.ts
@@ -0,0 +1,285 @@
+import crypto from 'crypto';
+import fs from 'fs';
+import path from 'path';
+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 { default: tailwindcss } = await import('@tailwindcss/jit');
+ postcssPlugins.push(tailwindcss());
+ } catch (err) {
+ 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(compileOptions.astroConfig.projectRoot.pathname, 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.codeStart.includes(`' ${scopedClass}'`)) {
+ // MustacheTag
+ attr.value[k].expression.codeStart = `(${attr.value[k].expression.codeStart}) + ' ${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 });
+ }
+ });
+ },
+ };
+}