Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/rust-compiler-experimental.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'astro': minor
---

Adds a new `experimental.rustCompiler` flag to opt into the experimental Rust-based Astro compiler (`@astrojs/compiler-rs`). When enabled, `@astrojs/compiler-rs` must be installed as a dev dependency.

This new compiler
1 change: 1 addition & 0 deletions packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@
},
"devDependencies": {
"@astrojs/check": "workspace:*",
"@astrojs/compiler-rs": "^0.1.0",
"@playwright/test": "1.58.2",
"@types/aria-query": "^5.0.4",
"@types/cssesc": "^3.0.2",
Expand Down
156 changes: 156 additions & 0 deletions packages/astro/src/core/compile/compile-rs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { fileURLToPath } from 'node:url';
import type { ResolvedConfig } from 'vite';
import type { AstroConfig } from '../../types/public/config.js';
import type { AstroError } from '../errors/errors.js';
import { AggregateError, CompilerError } from '../errors/errors.js';
import { AstroErrorData } from '../errors/index.js';
import { normalizePath, resolvePath } from '../viteUtils.js';
import { createStylePreprocessor, type PartialCompileCssResult } from './style.js';
import type { CompileCssResult } from './types.js';

export interface CompileProps {
astroConfig: AstroConfig;
viteConfig: ResolvedConfig;
toolbarEnabled: boolean;
filename: string;
source: string;
}

export interface CompileResult {
code: string;
map: string;
scope: string;
css: CompileCssResult[];
scripts: any[];
hydratedComponents: any[];
clientOnlyComponents: any[];
serverComponents: any[];
containsHead: boolean;
propagation: boolean;
styleError: string[];
diagnostics: any[];
}

export async function compile({
astroConfig,
viteConfig,
toolbarEnabled,
filename,
source,
}: CompileProps): Promise<CompileResult> {
const { preprocessStyles, transform } = await import('@astrojs/compiler-rs');

// Because `@astrojs/compiler-rs` can't return the dependencies for each style transformed,
// we need to use an external array to track the dependencies whenever preprocessing is called,
// and we'll rebuild the final `css` result after transformation.
const cssPartialCompileResults: PartialCompileCssResult[] = [];
const cssTransformErrors: AstroError[] = [];
let transformResult: any;

try {
// Transform from `.astro` to valid `.js`
// use `sourcemap: "both"` so that sourcemap is included in the code
// result passed to esbuild, but also available in the catch handler.

// Step 1: Preprocess styles (async — calls Vite's preprocessCSS)
const preprocessedStyles = await preprocessStyles(
source,
createStylePreprocessor({
filename,
viteConfig,
astroConfig,
cssPartialCompileResults,
cssTransformErrors,
}),
);

// Step 2: Transform (always sync)
transformResult = transform(source, {
compact: astroConfig.compressHTML,
filename,
normalizedFilename: normalizeFilename(filename, astroConfig.root),
sourcemap: 'both',
internalURL: 'astro/compiler-runtime',
// TODO: remove in Astro v7
astroGlobalArgs: JSON.stringify(astroConfig.site),
scopedStyleStrategy: astroConfig.scopedStyleStrategy,
resultScopedSlot: true,
transitionsAnimationURL: 'astro/components/viewtransitions.css',
annotateSourceFile:
viteConfig.command === 'serve' &&
astroConfig.devToolbar &&
astroConfig.devToolbar.enabled &&
toolbarEnabled,
preprocessedStyles,
resolvePath(specifier) {
return resolvePath(specifier, filename);
},
});
} catch (err: any) {
// The compiler should be able to handle errors by itself, however
// for the rare cases where it can't let's directly throw here with as much info as possible
throw new CompilerError({
...AstroErrorData.UnknownCompilerError,
message: err.message ?? 'Unknown compiler error',
stack: err.stack,
location: {
file: filename,
},
});
}

handleCompileResultErrors(filename, transformResult, cssTransformErrors);

return {
...transformResult,
css: transformResult.css.map((code: string, i: number) => ({
...cssPartialCompileResults[i],
code,
})),
};
}

function handleCompileResultErrors(
filename: string,
result: any,
cssTransformErrors: AstroError[],
) {
const compilerError = result.diagnostics.find((diag: any) => diag.severity === 'error');

if (compilerError) {
throw new CompilerError({
name: 'CompilerError',
message: compilerError.text,
location: {
line: compilerError.labels[0].line,
column: compilerError.labels[0].column,
file: filename,
},
hint: compilerError.hint,
});
}

switch (cssTransformErrors.length) {
case 0:
break;
case 1: {
throw cssTransformErrors[0];
}
default: {
throw new AggregateError({
...cssTransformErrors[0],
errors: cssTransformErrors,
});
}
}
}

function normalizeFilename(filename: string, root: URL) {
const normalizedFilename = normalizePath(filename);
const normalizedRoot = normalizePath(fileURLToPath(root));
if (normalizedFilename.startsWith(normalizedRoot)) {
return normalizedFilename.slice(normalizedRoot.length - 1);
} else {
return normalizedFilename;
}
}
17 changes: 15 additions & 2 deletions packages/astro/src/core/compile/style.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import fs from 'node:fs';
import type { TransformOptions } from '@astrojs/compiler';
import { preprocessCSS, type ResolvedConfig } from 'vite';
import type { AstroConfig } from '../../types/public/config.js';
import { AstroErrorData, CSSError, positionAt } from '../errors/index.js';
Expand All @@ -8,6 +7,20 @@ import type { CompileCssResult } from './types.js';

export type PartialCompileCssResult = Pick<CompileCssResult, 'isGlobal' | 'dependencies'>;

interface PreprocessorResult {
code: string;
map?: string;
}

interface PreprocessorError {
error: string;
}

export type PreprocessStyleFn = (
content: string,
attrs: Record<string, string>,
) => Promise<PreprocessorResult | PreprocessorError>;

/**
* Rewrites absolute URLs in CSS to include the base path.
*
Expand Down Expand Up @@ -90,7 +103,7 @@ export function createStylePreprocessor({
astroConfig: AstroConfig;
cssPartialCompileResults: Partial<CompileCssResult>[];
cssTransformErrors: Error[];
}): TransformOptions['preprocessStyle'] {
}): PreprocessStyleFn {
let processedStylesCount = 0;

return async (content, attrs) => {
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/core/config/schemas/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export const ASTRO_CONFIG_DEFAULTS = {
contentIntellisense: false,
chromeDevtoolsWorkspace: false,
svgo: false,
rustCompiler: false,
},
} satisfies AstroUserConfig & { server: { open: boolean } };

Expand Down Expand Up @@ -493,6 +494,7 @@ export const AstroConfigSchema = z.object({
.union([z.boolean(), z.custom<SvgoConfig>((value) => value && typeof value === 'object')])
.optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.svgo),
rustCompiler: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.rustCompiler),
})
.prefault({}),
legacy: z
Expand Down
27 changes: 27 additions & 0 deletions packages/astro/src/types/public/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2763,6 +2763,33 @@ export interface AstroUserConfig<
* See the [experimental SVGO optimization docs](https://docs.astro.build/en/reference/experimental-flags/svg-optimization/) for more information.
*/
svgo?: boolean | SvgoConfig;

/**
* @name experimental.rustCompiler
* @type {boolean}
* @default `false`
* @version 5.x
* @description
*
* Enables the experimental Rust-based Astro compiler (`@astrojs/compiler-rs`) as a replacement
* for the default Go-based compiler.
*
* The Rust compiler is faster and produces JavaScript output directly, removing the need for
* an additional esbuild TypeScript transform step. It requires installing `@astrojs/compiler-rs`
* as a dev dependency.
*
* ```js
* // astro.config.mjs
* import { defineConfig } from 'astro/config';
*
* export default defineConfig({
* experimental: {
* rustCompiler: true,
* },
* });
* ```
*/
rustCompiler?: boolean;
};
}

Expand Down
52 changes: 52 additions & 0 deletions packages/astro/src/vite-plugin-astro/compile-rs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { SourceMapInput } from 'rollup';
import { type CompileProps, type CompileResult, compile } from '../core/compile/compile-rs.js';
import { getFileInfo } from '../vite-plugin-utils/index.js';
import type { CompileMetadata } from './types.js';

interface CompileAstroOption {
compileProps: CompileProps;
astroFileToCompileMetadata: Map<string, CompileMetadata>;
}

export interface CompileAstroResult extends Omit<CompileResult, 'map'> {
map: SourceMapInput;
}

export async function compileAstro({
compileProps,
astroFileToCompileMetadata,
}: CompileAstroOption): Promise<CompileAstroResult> {
const transformResult = await compile(compileProps);

const { fileId: file, fileUrl: url } = getFileInfo(
compileProps.filename,
compileProps.astroConfig,
);

let SUFFIX = '';
SUFFIX += `\nconst $$file = ${JSON.stringify(file)};\nconst $$url = ${JSON.stringify(
url,
)};export { $$file as file, $$url as url };\n`;

// Add HMR handling in dev mode.
if (!compileProps.viteConfig.isProduction) {
let i = 0;
while (i < transformResult.scripts.length) {
SUFFIX += `import "${compileProps.filename}?astro&type=script&index=${i}&lang.ts";`;
i++;
}
}

// Attach compile metadata to map for use by virtual modules
astroFileToCompileMetadata.set(compileProps.filename, {
originalCode: compileProps.source,
css: transformResult.css,
scripts: transformResult.scripts,
});

return {
...transformResult,
code: transformResult.code + SUFFIX,
map: transformResult.map || null,
};
}
3 changes: 2 additions & 1 deletion packages/astro/src/vite-plugin-astro/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { AstroConfig } from '../types/public/config.js';
import { getFileInfo } from '../vite-plugin-utils/index.js';
import type { CompileMetadata } from './types.js';
import { frontmatterRE } from './utils.js';
import type { SourceMapInput } from 'rollup';

interface CompileAstroOption {
compileProps: CompileProps;
Expand All @@ -13,7 +14,7 @@ interface CompileAstroOption {
}

export interface CompileAstroResult extends Omit<CompileResult, 'map'> {
map: ESBuildTransformResult['map'];
map: SourceMapInput;
}

interface EnhanceCompilerErrorOptions {
Expand Down
23 changes: 16 additions & 7 deletions packages/astro/src/vite-plugin-astro/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { AstroSettings } from '../types/astro.js';
import type { AstroConfig } from '../types/public/config.js';
import { normalizeFilename, specialQueriesRE } from '../vite-plugin-utils/index.js';
import { type CompileAstroResult, compileAstro } from './compile.js';
import { compileAstro as compileAstroRs } from './compile-rs.js';
import { handleHotUpdate } from './hmr.js';
import { parseAstroRequest } from './query.js';
import type { PluginMetadata as AstroPluginMetadata, CompileMetadata } from './types.js';
Expand Down Expand Up @@ -88,16 +89,24 @@ export default function astro({ settings, logger }: AstroPluginOptions): vite.Pl
},
async configResolved(viteConfig) {
const toolbarEnabled = await settings.preferences.get('devToolbar.enabled');
const useRustCompiler = config.experimental.rustCompiler;
// Initialize `compile` function to simplify usage later
compile = (code, filename) => {
const compileProps = {
astroConfig: config,
viteConfig,
toolbarEnabled,
filename,
source: code,
};
if (useRustCompiler) {
return compileAstroRs({
compileProps,
astroFileToCompileMetadata,
});
}
return compileAstro({
compileProps: {
astroConfig: config,
viteConfig,
toolbarEnabled,
filename,
source: code,
},
compileProps,
astroFileToCompileMetadata,
logger,
});
Expand Down
Loading
Loading