summaryrefslogtreecommitdiff
path: root/src/compiler
diff options
context:
space:
mode:
Diffstat (limited to 'src/compiler')
-rw-r--r--src/compiler/codegen/content.ts78
-rw-r--r--src/compiler/codegen/index.ts686
-rw-r--r--src/compiler/codegen/utils.ts39
-rw-r--r--src/compiler/index.ts176
-rw-r--r--src/compiler/markdown/micromark-collect-headers.ts38
-rw-r--r--src/compiler/markdown/micromark-encode.ts36
-rw-r--r--src/compiler/markdown/micromark-mdx-astro.ts22
-rw-r--r--src/compiler/markdown/micromark.d.ts11
-rw-r--r--src/compiler/transform/doctype.ts36
-rw-r--r--src/compiler/transform/index.ts100
-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.ts89
-rw-r--r--src/compiler/transform/styles.ts290
14 files changed, 0 insertions, 1750 deletions
diff --git a/src/compiler/codegen/content.ts b/src/compiler/codegen/content.ts
deleted file mode 100644
index fb8f9e307..000000000
--- a/src/compiler/codegen/content.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import path from 'path';
-import { fdir, PathsOutput } from 'fdir';
-
-/**
- * Handling for import.meta.glob and import.meta.globEager
- */
-
-interface GlobOptions {
- namespace: string;
- filename: string;
-}
-
-interface GlobResult {
- /** Array of import statements to inject */
- imports: Set<string>;
- /** Replace original code with */
- code: string;
-}
-
-const crawler = new fdir();
-
-/** General glob handling */
-function globSearch(spec: string, { filename }: { filename: string }): string[] {
- try {
- // Note: fdir’s glob requires you to do some work finding the closest non-glob folder.
- // For example, this fails: .glob("./post/*.md").crawl("/…/src/pages") ❌
- // …but this doesn’t: .glob("*.md").crawl("/…/src/pages/post") ✅
- let globDir = '';
- let glob = spec;
- for (const part of spec.split('/')) {
- if (!part.includes('*')) {
- // iterate through spec until first '*' is reached
- globDir = path.posix.join(globDir, part); // this must be POSIX-style
- glob = glob.replace(`${part}/`, ''); // move parent dirs off spec, and onto globDir
- } else {
- // at first '*', exit
- break;
- }
- }
-
- const cwd = path.join(path.dirname(filename), globDir.replace(/\//g, path.sep)); // this must match OS (could be '/' or '\')
- let found = crawler.glob(glob).crawl(cwd).sync() as PathsOutput;
- if (!found.length) {
- throw new Error(`No files matched "${spec}" from ${filename}`);
- }
- return found.map((importPath) => {
- if (importPath.startsWith('http') || importPath.startsWith('.')) return importPath;
- return `./` + globDir + '/' + importPath;
- });
- } catch (err) {
- throw new Error(`No files matched "${spec}" from ${filename}`);
- }
-}
-
-/** Astro.fetchContent() */
-export function fetchContent(spec: string, { namespace, filename }: GlobOptions): GlobResult {
- let code = '';
- const imports = new Set<string>();
- const importPaths = globSearch(spec, { filename });
-
- // gather imports
- importPaths.forEach((importPath, j) => {
- const id = `${namespace}_${j}`;
- imports.add(`import { __content as ${id} } from '${importPath}';`);
-
- // add URL if this appears within the /pages/ directory (probably can be improved)
- const fullPath = path.resolve(path.dirname(filename), importPath);
- if (fullPath.includes(`${path.sep}pages${path.sep}`)) {
- const url = importPath.replace(/^\./, '').replace(/\.md$/, '');
- imports.add(`${id}.url = '${url}';`);
- }
- });
-
- // generate replacement code
- code += `${namespace} = [${importPaths.map((_, j) => `${namespace}_${j}`).join(',')}];\n`;
-
- return { imports, code };
-}
diff --git a/src/compiler/codegen/index.ts b/src/compiler/codegen/index.ts
deleted file mode 100644
index 5113799d6..000000000
--- a/src/compiler/codegen/index.ts
+++ /dev/null
@@ -1,686 +0,0 @@
-import type { CompileOptions } from '../../@types/compiler';
-import type { AstroConfig, ValidExtensionPlugins } from '../../@types/astro';
-import type { Ast, Script, Style, TemplateNode } from '../../parser/interfaces';
-import type { TransformResult } from '../../@types/astro';
-
-import eslexer from 'es-module-lexer';
-import esbuild from 'esbuild';
-import path from 'path';
-import { walk } from 'estree-walker';
-import _babelGenerator from '@babel/generator';
-import babelParser from '@babel/parser';
-import { codeFrameColumns } from '@babel/code-frame';
-import * as babelTraverse from '@babel/traverse';
-import { ImportDeclaration, ExportNamedDeclaration, VariableDeclarator, Identifier } from '@babel/types';
-import { warn } from '../../logger.js';
-import { fetchContent } from './content.js';
-import { isFetchContent } from './utils.js';
-import { yellow } from 'kleur/colors';
-
-const traverse: typeof babelTraverse.default = (babelTraverse.default as any).default;
-const babelGenerator: typeof _babelGenerator =
- // @ts-ignore
- _babelGenerator.default;
-const { transformSync } = esbuild;
-
-interface Attribute {
- start: number;
- end: number;
- type: 'Attribute';
- name: string;
- value: TemplateNode[] | boolean;
-}
-
-interface CodeGenOptions {
- compileOptions: CompileOptions;
- filename: string;
- fileID: string;
-}
-
-/** Format Astro internal import URL */
-function internalImport(internalPath: string) {
- return `/_astro_internal/${internalPath}`;
-}
-
-/** Retrieve attributes from TemplateNode */
-function getAttributes(attrs: Attribute[]): Record<string, string> {
- let result: Record<string, string> = {};
- for (const attr of attrs) {
- if (attr.value === true) {
- result[attr.name] = JSON.stringify(attr.value);
- continue;
- }
- if (attr.value === false || attr.value === undefined) {
- // note: attr.value shouldn’t be `undefined`, but a bad transform would cause a compile error here, so prevent that
- continue;
- }
- if (attr.value.length > 1) {
- result[attr.name] =
- '(' +
- attr.value
- .map((v: TemplateNode) => {
- if (v.content) {
- return v.content;
- } else {
- return JSON.stringify(getTextFromAttribute(v));
- }
- })
- .join('+') +
- ')';
- continue;
- }
- const val = attr.value[0];
- if (!val) {
- result[attr.name] = '(' + val + ')';
- continue;
- }
- switch (val.type) {
- case 'MustacheTag': {
- // FIXME: this won't work when JSX element can appear in attributes (rare but possible).
- result[attr.name] = '(' + val.expression.codeChunks[0] + ')';
- continue;
- }
- case 'Text':
- result[attr.name] = JSON.stringify(getTextFromAttribute(val));
- continue;
- default:
- throw new Error(`UNKNOWN: ${val.type}`);
- }
- }
- return result;
-}
-
-/** Get value from a TemplateNode Attribute (text attributes only!) */
-function getTextFromAttribute(attr: any): string {
- switch (attr.type) {
- case 'Text': {
- if (attr.raw !== undefined) {
- return attr.raw;
- }
- if (attr.data !== undefined) {
- return attr.data;
- }
- break;
- }
- case 'MustacheTag': {
- // FIXME: this won't work when JSX element can appear in attributes (rare but possible).
- return attr.expression.codeChunks[0];
- }
- }
- throw new Error(`Unknown attribute type ${attr.type}`);
-}
-
-/** Convert TemplateNode attributes to string */
-function generateAttributes(attrs: Record<string, string>): string {
- let result = '{';
- for (const [key, val] of Object.entries(attrs)) {
- result += JSON.stringify(key) + ':' + val + ',';
- }
- return result + '}';
-}
-
-interface ComponentInfo {
- type: string;
- url: string;
- plugin: string | undefined;
-}
-
-const defaultExtensions: Readonly<Record<string, ValidExtensionPlugins>> = {
- '.astro': 'astro',
- '.jsx': 'react',
- '.tsx': 'react',
- '.vue': 'vue',
- '.svelte': 'svelte',
-};
-
-type DynamicImportMap = Map<'vue' | 'react' | 'react-dom' | 'preact' | 'svelte', string>;
-
-interface GetComponentWrapperOptions {
- filename: string;
- astroConfig: AstroConfig;
- dynamicImports: DynamicImportMap;
-}
-
-/** Generate Astro-friendly component import */
-function getComponentWrapper(_name: string, { type, plugin, url }: ComponentInfo, opts: GetComponentWrapperOptions) {
- const { astroConfig, dynamicImports, filename } = opts;
- const { astroRoot } = astroConfig;
- const [name, kind] = _name.split(':');
- const currFileUrl = new URL(`file://${filename}`);
-
- if (!plugin) {
- throw new Error(`No supported plugin found for ${type ? `extension ${type}` : `${url} (try adding an extension)`}`);
- }
-
- const getComponentUrl = (ext = '.js') => {
- const outUrl = new URL(url, currFileUrl);
- return '/_astro/' + path.posix.relative(astroRoot.pathname, outUrl.pathname).replace(/\.[^.]+$/, ext);
- };
-
- switch (plugin) {
- case 'astro': {
- if (kind) {
- throw new Error(`Astro does not support :${kind}`);
- }
- return {
- wrapper: name,
- wrapperImport: ``,
- };
- }
- case 'preact': {
- if (['load', 'idle', 'visible'].includes(kind)) {
- return {
- wrapper: `__preact_${kind}(${name}, ${JSON.stringify({
- componentUrl: getComponentUrl(),
- componentExport: 'default',
- frameworkUrls: {
- preact: dynamicImports.get('preact'),
- },
- })})`,
- wrapperImport: `import {__preact_${kind}} from '${internalImport('render/preact.js')}';`,
- };
- }
-
- return {
- wrapper: `__preact_static(${name})`,
- wrapperImport: `import {__preact_static} from '${internalImport('render/preact.js')}';`,
- };
- }
- case 'react': {
- if (['load', 'idle', 'visible'].includes(kind)) {
- return {
- wrapper: `__react_${kind}(${name}, ${JSON.stringify({
- componentUrl: getComponentUrl(),
- componentExport: 'default',
- frameworkUrls: {
- react: dynamicImports.get('react'),
- 'react-dom': dynamicImports.get('react-dom'),
- },
- })})`,
- wrapperImport: `import {__react_${kind}} from '${internalImport('render/react.js')}';`,
- };
- }
-
- return {
- wrapper: `__react_static(${name})`,
- wrapperImport: `import {__react_static} from '${internalImport('render/react.js')}';`,
- };
- }
- case 'svelte': {
- if (['load', 'idle', 'visible'].includes(kind)) {
- return {
- wrapper: `__svelte_${kind}(${name}, ${JSON.stringify({
- componentUrl: getComponentUrl('.svelte.js'),
- componentExport: 'default',
- frameworkUrls: {
- 'svelte-runtime': internalImport('runtime/svelte.js'),
- },
- })})`,
- wrapperImport: `import {__svelte_${kind}} from '${internalImport('render/svelte.js')}';`,
- };
- }
-
- return {
- wrapper: `__svelte_static(${name})`,
- wrapperImport: `import {__svelte_static} from '${internalImport('render/svelte.js')}';`,
- };
- }
- case 'vue': {
- if (['load', 'idle', 'visible'].includes(kind)) {
- return {
- wrapper: `__vue_${kind}(${name}, ${JSON.stringify({
- componentUrl: getComponentUrl('.vue.js'),
- componentExport: 'default',
- frameworkUrls: {
- vue: dynamicImports.get('vue'),
- },
- })})`,
- wrapperImport: `import {__vue_${kind}} from '${internalImport('render/vue.js')}';`,
- };
- }
-
- return {
- wrapper: `__vue_static(${name})`,
- wrapperImport: `import {__vue_static} from '${internalImport('render/vue.js')}';`,
- };
- }
- default: {
- throw new Error(`Unknown component type`);
- }
- }
-}
-
-/** Evaluate expression (safely) */
-function compileExpressionSafe(raw: string): string {
- let { code } = transformSync(raw, {
- loader: 'tsx',
- jsxFactory: 'h',
- jsxFragment: 'Fragment',
- charset: 'utf8',
- });
- return code;
-}
-
-/** Build dependency map of dynamic component runtime frameworks */
-async function acquireDynamicComponentImports(plugins: Set<ValidExtensionPlugins>, resolvePackageUrl: (s: string) => Promise<string>): Promise<DynamicImportMap> {
- const importMap: DynamicImportMap = new Map();
- for (let plugin of plugins) {
- switch (plugin) {
- case 'vue': {
- importMap.set('vue', await resolvePackageUrl('vue'));
- break;
- }
- case 'react': {
- importMap.set('react', await resolvePackageUrl('react'));
- importMap.set('react-dom', await resolvePackageUrl('react-dom'));
- break;
- }
- case 'preact': {
- importMap.set('preact', await resolvePackageUrl('preact'));
- break;
- }
- case 'svelte': {
- importMap.set('svelte', await resolvePackageUrl('svelte'));
- break;
- }
- }
- }
- return importMap;
-}
-
-type Components = Record<string, { type: string; url: string; plugin: string | undefined }>;
-
-interface CompileResult {
- script: string;
- componentPlugins: Set<ValidExtensionPlugins>;
- createCollection?: string;
-}
-
-interface CodegenState {
- filename: string;
- components: Components;
- css: string[];
- importExportStatements: Set<string>;
- dynamicImports: DynamicImportMap;
-}
-
-/** Compile/prepare Astro frontmatter scripts */
-function compileModule(module: Script, state: CodegenState, compileOptions: CompileOptions): CompileResult {
- const { extensions = defaultExtensions } = compileOptions;
-
- const componentImports: ImportDeclaration[] = [];
- const componentProps: VariableDeclarator[] = [];
- const componentExports: ExportNamedDeclaration[] = [];
-
- const contentImports = new Map<string, { spec: string; declarator: string }>();
-
- let script = '';
- let propsStatement = '';
- let contentCode = ''; // code for handling Astro.fetchContent(), if any;
- let createCollection = ''; // function for executing collection
- const componentPlugins = new Set<ValidExtensionPlugins>();
-
- if (module) {
- const parseOptions: babelParser.ParserOptions = {
- sourceType: 'module',
- plugins: ['jsx', 'typescript', 'topLevelAwait'],
- };
- let parseResult;
- try {
- parseResult = babelParser.parse(module.content, parseOptions);
- } catch (err) {
- const location = { start: err.loc };
- const frame = codeFrameColumns(module.content, location);
- err.frame = frame;
- err.filename = state.filename;
- err.start = err.loc;
- throw err;
- }
- const program = parseResult.program;
-
- const { body } = program;
- let i = body.length;
- while (--i >= 0) {
- const node = body[i];
- switch (node.type) {
- case 'ExportNamedDeclaration': {
- if (!node.declaration) break;
- // const replacement = extract_exports(node);
-
- if (node.declaration.type === 'VariableDeclaration') {
- // case 1: prop (export let title)
-
- const declaration = node.declaration.declarations[0];
- if ((declaration.id as Identifier).name === '__layout' || (declaration.id as Identifier).name === '__content') {
- componentExports.push(node);
- } else {
- componentProps.push(declaration);
- }
- body.splice(i, 1);
- } else if (node.declaration.type === 'FunctionDeclaration') {
- // case 2: createCollection (export async function)
- if (!node.declaration.id || node.declaration.id.name !== 'createCollection') break;
- createCollection = module.content.substring(node.declaration.start || 0, node.declaration.end || 0);
-
- // remove node
- body.splice(i, 1);
- }
- break;
- }
- case 'FunctionDeclaration': {
- break;
- }
- case 'ImportDeclaration': {
- componentImports.push(node);
- body.splice(i, 1); // remove node
- break;
- }
- case 'VariableDeclaration': {
- for (const declaration of node.declarations) {
- // only select Astro.fetchContent() calls here. this utility filters those out for us.
- if (!isFetchContent(declaration)) continue;
-
- // remove node
- body.splice(i, 1);
-
- // a bit of munging
- let { id, init } = declaration;
- if (!id || !init || id.type !== 'Identifier') continue;
- if (init.type === 'AwaitExpression') {
- init = init.argument;
- const shortname = path.relative(compileOptions.astroConfig.projectRoot.pathname, state.filename);
- warn(compileOptions.logging, shortname, yellow('awaiting Astro.fetchContent() not necessary'));
- }
- if (init.type !== 'CallExpression') continue;
-
- // gather data
- const namespace = id.name;
-
- if ((init as any).arguments[0].type !== 'StringLiteral') {
- throw new Error(`[Astro.fetchContent] Only string literals allowed, ex: \`Astro.fetchContent('./post/*.md')\`\n ${state.filename}`);
- }
- const spec = (init as any).arguments[0].value;
- if (typeof spec === 'string') contentImports.set(namespace, { spec, declarator: node.kind });
- }
- break;
- }
- }
- }
-
- for (const componentImport of componentImports) {
- const importUrl = componentImport.source.value;
- const componentType = path.posix.extname(importUrl);
- const specifier = componentImport.specifiers[0];
- if (!specifier) continue; // this is unused
- // set componentName to default import if used (user), or use filename if no default import (mostly internal use)
- const componentName = specifier.type === 'ImportDefaultSpecifier' ? specifier.local.name : path.posix.basename(importUrl, componentType);
- const plugin = extensions[componentType] || defaultExtensions[componentType];
- state.components[componentName] = {
- type: componentType,
- plugin,
- url: importUrl,
- };
- if (plugin) {
- componentPlugins.add(plugin);
- }
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- state.importExportStatements.add(module.content.slice(componentImport.start!, componentImport.end!));
- }
- for (const componentImport of componentExports) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- state.importExportStatements.add(module.content.slice(componentImport.start!, componentImport.end!));
- }
-
- if (componentProps.length > 0) {
- propsStatement = 'let {';
- for (const componentExport of componentProps) {
- propsStatement += `${(componentExport.id as Identifier).name}`;
- if (componentExport.init) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- propsStatement += `= ${babelGenerator(componentExport.init!).code}`;
- }
- propsStatement += `,`;
- }
- propsStatement += `} = props;\n`;
- }
-
- // handle createCollection, if any
- if (createCollection) {
- // TODO: improve this? while transforming in-place isn’t great, this happens at most once per-route
- const ast = babelParser.parse(createCollection, {
- sourceType: 'module',
- });
- traverse(ast, {
- enter({ node }) {
- switch (node.type) {
- case 'VariableDeclaration': {
- for (const declaration of node.declarations) {
- // only select Astro.fetchContent() calls here. this utility filters those out for us.
- if (!isFetchContent(declaration)) continue;
-
- // a bit of munging
- let { id, init } = declaration;
- if (!id || !init || id.type !== 'Identifier') continue;
- if (init.type === 'AwaitExpression') {
- init = init.argument;
- const shortname = path.relative(compileOptions.astroConfig.projectRoot.pathname, state.filename);
- warn(compileOptions.logging, shortname, yellow('awaiting Astro.fetchContent() not necessary'));
- }
- if (init.type !== 'CallExpression') continue;
-
- // gather data
- const namespace = id.name;
-
- if ((init as any).arguments[0].type !== 'StringLiteral') {
- throw new Error(`[Astro.fetchContent] Only string literals allowed, ex: \`Astro.fetchContent('./post/*.md')\`\n ${state.filename}`);
- }
- const spec = (init as any).arguments[0].value;
- if (typeof spec !== 'string') break;
-
- const globResult = fetchContent(spec, { namespace, filename: state.filename });
-
- let imports = '';
- for (const importStatement of globResult.imports) {
- imports += importStatement + '\n';
- }
-
- createCollection =
- imports + '\nexport ' + createCollection.substring(0, declaration.start || 0) + globResult.code + createCollection.substring(declaration.end || 0);
- }
- break;
- }
- }
- },
- });
- }
-
- // Astro.fetchContent()
- for (const [namespace, { spec }] of contentImports.entries()) {
- const globResult = fetchContent(spec, { namespace, filename: state.filename });
- for (const importStatement of globResult.imports) {
- state.importExportStatements.add(importStatement);
- }
- contentCode += globResult.code;
- }
-
- script = propsStatement + contentCode + babelGenerator(program).code;
- }
-
- return {
- script,
- componentPlugins,
- createCollection: createCollection || undefined,
- };
-}
-
-/** Compile styles */
-function compileCss(style: Style, state: CodegenState) {
- walk(style, {
- enter(node: TemplateNode) {
- if (node.type === 'Style') {
- state.css.push(node.content.styles); // if multiple <style> tags, combine together
- this.skip();
- }
- },
- leave(node: TemplateNode) {
- if (node.type === 'Style') {
- this.remove(); // this will be optimized in a global CSS file; remove so it‘s not accidentally inlined
- }
- },
- });
-}
-
-/** Compile page markup */
-function compileHtml(enterNode: TemplateNode, state: CodegenState, compileOptions: CompileOptions) {
- const { components, css, importExportStatements, dynamicImports, filename } = state;
- const { astroConfig } = compileOptions;
-
- let outSource = '';
- walk(enterNode, {
- enter(node: TemplateNode) {
- switch (node.type) {
- case 'Expression': {
- let children: string[] = [];
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- for (const child of node.children!) {
- children.push(compileHtml(child, state, compileOptions));
- }
- let raw = '';
- let nextChildIndex = 0;
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- for (const chunk of node.codeChunks!) {
- raw += chunk;
- if (nextChildIndex < children.length) {
- raw += children[nextChildIndex++];
- }
- }
- // TODO Do we need to compile this now, or should we compile the entire module at the end?
- let code = compileExpressionSafe(raw).trim().replace(/\;$/, '');
- outSource += `,(${code})`;
- this.skip();
- break;
- }
- case 'MustacheTag':
- case 'Comment':
- return;
- case 'Fragment':
- break;
- case 'Slot':
- case 'Head':
- case 'InlineComponent':
- case 'Title':
- case 'Element': {
- const name: string = node.name;
- if (!name) {
- throw new Error('AHHHH');
- }
- const attributes = getAttributes(node.attributes);
-
- outSource += outSource === '' ? '' : ',';
- if (node.type === 'Slot') {
- outSource += `(children`;
- return;
- }
- const COMPONENT_NAME_SCANNER = /^[A-Z]/;
- if (!COMPONENT_NAME_SCANNER.test(name)) {
- outSource += `h("${name}", ${attributes ? generateAttributes(attributes) : 'null'}`;
- return;
- }
- const [componentName, componentKind] = name.split(':');
- const componentImportData = components[componentName];
- if (!componentImportData) {
- throw new Error(`Unknown Component: ${componentName}`);
- }
- const { wrapper, wrapperImport } = getComponentWrapper(name, components[componentName], { astroConfig, dynamicImports, filename });
- if (wrapperImport) {
- importExportStatements.add(wrapperImport);
- }
-
- outSource += `h(${wrapper}, ${attributes ? generateAttributes(attributes) : 'null'}`;
- return;
- }
- case 'Attribute': {
- this.skip();
- return;
- }
- case 'Style': {
- css.push(node.content.styles); // if multiple <style> tags, combine together
- this.skip();
- return;
- }
- case 'Text': {
- const text = getTextFromAttribute(node);
- if (!text.trim()) {
- return;
- }
- outSource += ',' + JSON.stringify(text);
- return;
- }
- default:
- throw new Error('Unexpected (enter) node type: ' + node.type);
- }
- },
- leave(node, parent, prop, index) {
- switch (node.type) {
- case 'Text':
- case 'Attribute':
- case 'Comment':
- case 'Fragment':
- case 'Expression':
- case 'MustacheTag':
- return;
- case 'Slot':
- case 'Head':
- case 'Body':
- case 'Title':
- case 'Element':
- case 'InlineComponent':
- outSource += ')';
- return;
- case 'Style': {
- this.remove(); // this will be optimized in a global CSS file; remove so it‘s not accidentally inlined
- return;
- }
- default:
- throw new Error('Unexpected (leave) node type: ' + node.type);
- }
- },
- });
-
- return outSource;
-}
-
-/**
- * Codegen
- * Step 3/3 in Astro SSR.
- * This is the final pass over a document AST before it‘s converted to an h() function
- * and handed off to Snowpack to build.
- * @param {Ast} AST The parsed AST to crawl
- * @param {object} CodeGenOptions
- */
-export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOptions): Promise<TransformResult> {
- await eslexer.init;
-
- const state: CodegenState = {
- filename,
- components: {},
- css: [],
- importExportStatements: new Set(),
- dynamicImports: new Map(),
- };
-
- const { script, componentPlugins, createCollection } = compileModule(ast.module, state, compileOptions);
- state.dynamicImports = await acquireDynamicComponentImports(componentPlugins, compileOptions.resolvePackageUrl);
-
- compileCss(ast.css, state);
-
- const html = compileHtml(ast.html, state, compileOptions);
-
- return {
- script: script,
- imports: Array.from(state.importExportStatements),
- html,
- css: state.css.length ? state.css.join('\n\n') : undefined,
- createCollection,
- };
-}
diff --git a/src/compiler/codegen/utils.ts b/src/compiler/codegen/utils.ts
deleted file mode 100644
index e1c558bc4..000000000
--- a/src/compiler/codegen/utils.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * Codegen utils
- */
-
-import type { VariableDeclarator } from '@babel/types';
-
-/** Is this an import.meta.* built-in? You can pass an optional 2nd param to see if the name matches as well. */
-export function isImportMetaDeclaration(declaration: VariableDeclarator, metaName?: string): boolean {
- let { init } = declaration;
- if (!init) return false; // definitely not import.meta
- // this could be `await import.meta`; if so, evaluate that:
- if (init.type === 'AwaitExpression') {
- init = init.argument;
- }
- // continue evaluating
- if (init.type !== 'CallExpression' || init.callee.type !== 'MemberExpression' || init.callee.object.type !== 'MetaProperty') return false;
- // optional: if metaName specified, match that
- if (metaName && (init.callee.property.type !== 'Identifier' || init.callee.property.name !== metaName)) return false;
- return true;
-}
-
-/** Is this an Astro.fetchContent() call? */
-export function isFetchContent(declaration: VariableDeclarator): boolean {
- let { init } = declaration;
- if (!init) return false; // definitely not import.meta
- // this could be `await import.meta`; if so, evaluate that:
- if (init.type === 'AwaitExpression') {
- init = init.argument;
- }
- // continue evaluating
- if (
- init.type !== 'CallExpression' ||
- init.callee.type !== 'MemberExpression' ||
- (init.callee.object as any).name !== 'Astro' ||
- (init.callee.property as any).name !== 'fetchContent'
- )
- return false;
- return true;
-}
diff --git a/src/compiler/index.ts b/src/compiler/index.ts
deleted file mode 100644
index 7e7bfc4c6..000000000
--- a/src/compiler/index.ts
+++ /dev/null
@@ -1,176 +0,0 @@
-import type { CompileResult, TransformResult } from '../@types/astro';
-import type { CompileOptions } from '../@types/compiler.js';
-
-import path from 'path';
-import micromark from 'micromark';
-import gfmSyntax from 'micromark-extension-gfm';
-import matter from 'gray-matter';
-import gfmHtml from 'micromark-extension-gfm/html.js';
-
-import { parse } from '../parser/index.js';
-import { createMarkdownHeadersCollector } from './markdown/micromark-collect-headers.js';
-import { encodeMarkdown } from './markdown/micromark-encode.js';
-import { encodeAstroMdx } from './markdown/micromark-mdx-astro.js';
-import { transform } from './transform/index.js';
-import { codegen } from './codegen/index.js';
-
-/** Return Astro internal import URL */
-function internalImport(internalPath: string) {
- return `/_astro_internal/${internalPath}`;
-}
-
-interface ConvertAstroOptions {
- compileOptions: CompileOptions;
- filename: string;
- fileID: string;
-}
-
-/**
- * .astro -> .jsx
- * Core function processing .astro files. Initiates all 3 phases of compilation:
- * 1. Parse
- * 2. Transform
- * 3. Codegen
- */
-async function convertAstroToJsx(template: string, opts: ConvertAstroOptions): Promise<TransformResult> {
- const { filename } = opts;
-
- // 1. Parse
- const ast = parse(template, {
- filename,
- });
-
- // 2. Transform the AST
- await transform(ast, opts);
-
- // 3. Turn AST into JSX
- return await codegen(ast, opts);
-}
-
-/**
- * .md -> .astro source
- */
-export async function convertMdToAstroSource(contents: string): Promise<string> {
- const { data: frontmatterData, content } = matter(contents);
- const { headers, headersExtension } = createMarkdownHeadersCollector();
- const { htmlAstro, mdAstro } = encodeAstroMdx();
- const mdHtml = micromark(content, {
- allowDangerousHtml: true,
- extensions: [gfmSyntax(), ...htmlAstro],
- htmlExtensions: [gfmHtml, encodeMarkdown, headersExtension, mdAstro],
- });
-
- // TODO: Warn if reserved word is used in "frontmatterData"
- const contentData: any = {
- ...frontmatterData,
- headers,
- source: content,
- };
-
- let imports = '';
- for (let [ComponentName, specifier] of Object.entries(frontmatterData.import || {})) {
- imports += `import ${ComponentName} from '${specifier}';\n`;
- }
-
- // </script> can't be anywhere inside of a JS string, otherwise the HTML parser fails.
- // Break it up here so that the HTML parser won't detect it.
- const stringifiedSetupContext = JSON.stringify(contentData).replace(/\<\/script\>/g, `</scrip" + "t>`);
-
- return `---
- ${imports}
- ${frontmatterData.layout ? `import {__renderPage as __layout} from '${frontmatterData.layout}';` : 'const __layout = undefined;'}
- export const __content = ${stringifiedSetupContext};
----
-<section>${mdHtml}</section>`;
-}
-
-/**
- * .md -> .jsx
- * Core function processing Markdown, but along the way also calls convertAstroToJsx().
- */
-async function convertMdToJsx(
- contents: string,
- { compileOptions, filename, fileID }: { compileOptions: CompileOptions; filename: string; fileID: string }
-): Promise<TransformResult> {
- const raw = await convertMdToAstroSource(contents);
- const convertOptions = { compileOptions, filename, fileID };
- return await convertAstroToJsx(raw, convertOptions);
-}
-
-type SupportedExtensions = '.astro' | '.md';
-
-/** Given a file, process it either as .astro or .md. */
-async function transformFromSource(
- contents: string,
- { compileOptions, filename, projectRoot }: { compileOptions: CompileOptions; filename: string; projectRoot: string }
-): Promise<TransformResult> {
- const fileID = path.relative(projectRoot, filename);
- switch (path.extname(filename) as SupportedExtensions) {
- case '.astro':
- return await convertAstroToJsx(contents, { compileOptions, filename, fileID });
- case '.md':
- return await convertMdToJsx(contents, { compileOptions, filename, fileID });
- default:
- throw new Error('Not Supported!');
- }
-}
-
-/** Return internal code that gets processed in Snowpack */
-export async function compileComponent(
- source: string,
- { compileOptions, filename, projectRoot }: { compileOptions: CompileOptions; filename: string; projectRoot: string }
-): Promise<CompileResult> {
- const result = await transformFromSource(source, { compileOptions, filename, projectRoot });
-
- // return template
- let modJsx = `
-import fetch from 'node-fetch';
-
-// <script astro></script>
-${result.imports.join('\n')}
-
-// \`__render()\`: Render the contents of the Astro module.
-import { h, Fragment } from '${internalImport('h.js')}';
-const __astroRequestSymbol = Symbol('astro.request');
-async function __render(props, ...children) {
- const Astro = {
- request: props[__astroRequestSymbol]
- };
-
- ${result.script}
- return h(Fragment, null, ${result.html});
-}
-export default __render;
-
-${result.createCollection || ''}
-
-// \`__renderPage()\`: Render the contents of the Astro module as a page. This is a special flow,
-// triggered by loading a component directly by URL.
-export async function __renderPage({request, children, props}) {
- const currentChild = {
- layout: typeof __layout === 'undefined' ? undefined : __layout,
- content: typeof __content === 'undefined' ? undefined : __content,
- __render,
- };
-
- props[__astroRequestSymbol] = request;
- const childBodyResult = await currentChild.__render(props, children);
-
- // find layout, if one was given.
- if (currentChild.layout) {
- return currentChild.layout({
- request,
- props: {content: currentChild.content},
- children: [childBodyResult],
- });
- }
-
- return childBodyResult;
-};\n`;
-
- return {
- result,
- contents: modJsx,
- css: result.css,
- };
-}
diff --git a/src/compiler/markdown/micromark-collect-headers.ts b/src/compiler/markdown/micromark-collect-headers.ts
deleted file mode 100644
index 69781231a..000000000
--- a/src/compiler/markdown/micromark-collect-headers.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import slugger from 'github-slugger';
-
-/**
- * Create Markdown Headers Collector
- * NOTE: micromark has terrible TS types. Instead of fighting with the
- * limited/broken TS types that they ship, we just reach for our good friend, "any".
- */
-export function createMarkdownHeadersCollector() {
- const headers: any[] = [];
- let currentHeader: any;
- return {
- headers,
- headersExtension: {
- enter: {
- atxHeading(node: any) {
- currentHeader = {};
- headers.push(currentHeader);
- this.buffer();
- },
- atxHeadingSequence(node: any) {
- currentHeader.depth = this.sliceSerialize(node).length;
- },
- atxHeadingText(node: any) {
- currentHeader.text = this.sliceSerialize(node);
- },
- } as any,
- exit: {
- atxHeading(node: any) {
- currentHeader.slug = slugger.slug(currentHeader.text);
- this.resume();
- this.tag(`<h${currentHeader.depth} id="${currentHeader.slug}">`);
- this.raw(currentHeader.text);
- this.tag(`</h${currentHeader.depth}>`);
- },
- } as any,
- } as any,
- };
-}
diff --git a/src/compiler/markdown/micromark-encode.ts b/src/compiler/markdown/micromark-encode.ts
deleted file mode 100644
index 635ab3b54..000000000
--- a/src/compiler/markdown/micromark-encode.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import type { Token } from 'micromark/dist/shared-types';
-import type { MicromarkExtension, MicromarkExtensionContext } from '../../@types/micromark';
-
-const characterReferences = {
- '"': 'quot',
- '&': 'amp',
- '<': 'lt',
- '>': 'gt',
- '{': 'lbrace',
- '}': 'rbrace',
-};
-
-type EncodedChars = '"' | '&' | '<' | '>' | '{' | '}';
-
-/** Encode HTML entity */
-function encode(value: string): string {
- return value.replace(/["&<>{}]/g, (raw: string) => {
- return '&' + characterReferences[raw as EncodedChars] + ';';
- });
-}
-
-/** Encode Markdown node */
-function encodeToken(this: MicromarkExtensionContext) {
- const token: Token = arguments[0];
- const value = this.sliceSerialize(token);
- this.raw(encode(value));
-}
-
-const plugin: MicromarkExtension = {
- exit: {
- codeTextData: encodeToken,
- codeFlowValue: encodeToken,
- },
-};
-
-export { plugin as encodeMarkdown };
diff --git a/src/compiler/markdown/micromark-mdx-astro.ts b/src/compiler/markdown/micromark-mdx-astro.ts
deleted file mode 100644
index b978ad407..000000000
--- a/src/compiler/markdown/micromark-mdx-astro.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import type { MicromarkExtension } from '../../@types/micromark';
-import mdxExpression from 'micromark-extension-mdx-expression';
-import mdxJsx from 'micromark-extension-mdx-jsx';
-
-/**
- * Keep MDX.
- */
-export function encodeAstroMdx() {
- const extension: MicromarkExtension = {
- enter: {
- mdxJsxFlowTag(node: any) {
- const mdx = this.sliceSerialize(node);
- this.raw(mdx);
- },
- },
- };
-
- return {
- htmlAstro: [mdxExpression(), mdxJsx()],
- mdAstro: extension,
- };
-}
diff --git a/src/compiler/markdown/micromark.d.ts b/src/compiler/markdown/micromark.d.ts
deleted file mode 100644
index fd094306e..000000000
--- a/src/compiler/markdown/micromark.d.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-declare module 'micromark-extension-mdx-expression' {
- import type { HtmlExtension } from 'micromark/dist/shared-types';
-
- export default function (): HtmlExtension;
-}
-
-declare module 'micromark-extension-mdx-jsx' {
- import type { HtmlExtension } from 'micromark/dist/shared-types';
-
- export default function (): HtmlExtension;
-}
diff --git a/src/compiler/transform/doctype.ts b/src/compiler/transform/doctype.ts
deleted file mode 100644
index e871f5b48..000000000
--- a/src/compiler/transform/doctype.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-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',
- };
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- 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
deleted file mode 100644
index 02a98709b..000000000
--- a/src/compiler/transform/index.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-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)) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- 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)) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- 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
deleted file mode 100644
index aff1ec4f6..000000000
--- a/src/compiler/transform/module-scripts.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-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
deleted file mode 100644
index 23350869c..000000000
--- a/src/compiler/transform/postcss-scoped-styles/index.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-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
deleted file mode 100644
index 1bb024a84..000000000
--- a/src/compiler/transform/prism.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-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['"]/;
-/** escaping code samples that contain template string replacement parts, ${foo} or example. */
-function escape(code: string) {
- return code.replace(/[`$]/g, (match) => {
- return '\\' + match;
- });
-}
-/** default export - Transform prism */
-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',
- codeChunks: ['`' + escape(code) + '`'],
- 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
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 });
- }
- });
- },
- };
-}