diff --git a/.changeset/rust-compiler-experimental.md b/.changeset/rust-compiler-experimental.md new file mode 100644 index 000000000000..41f09b42b5f6 --- /dev/null +++ b/.changeset/rust-compiler-experimental.md @@ -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 diff --git a/packages/astro/package.json b/packages/astro/package.json index 497b5bde7548..262fac6f12b5 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -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", diff --git a/packages/astro/src/core/compile/compile-rs.ts b/packages/astro/src/core/compile/compile-rs.ts new file mode 100644 index 000000000000..e82a3138121f --- /dev/null +++ b/packages/astro/src/core/compile/compile-rs.ts @@ -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 { + 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; + } +} diff --git a/packages/astro/src/core/compile/style.ts b/packages/astro/src/core/compile/style.ts index d6e4dec983f6..c0d63c04e1ea 100644 --- a/packages/astro/src/core/compile/style.ts +++ b/packages/astro/src/core/compile/style.ts @@ -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'; @@ -8,6 +7,20 @@ import type { CompileCssResult } from './types.js'; export type PartialCompileCssResult = Pick; +interface PreprocessorResult { + code: string; + map?: string; +} + +interface PreprocessorError { + error: string; +} + +export type PreprocessStyleFn = ( + content: string, + attrs: Record, +) => Promise; + /** * Rewrites absolute URLs in CSS to include the base path. * @@ -90,7 +103,7 @@ export function createStylePreprocessor({ astroConfig: AstroConfig; cssPartialCompileResults: Partial[]; cssTransformErrors: Error[]; -}): TransformOptions['preprocessStyle'] { +}): PreprocessStyleFn { let processedStylesCount = 0; return async (content, attrs) => { diff --git a/packages/astro/src/core/config/schemas/base.ts b/packages/astro/src/core/config/schemas/base.ts index fa9863a6539a..8f550224cc41 100644 --- a/packages/astro/src/core/config/schemas/base.ts +++ b/packages/astro/src/core/config/schemas/base.ts @@ -104,6 +104,7 @@ export const ASTRO_CONFIG_DEFAULTS = { contentIntellisense: false, chromeDevtoolsWorkspace: false, svgo: false, + rustCompiler: false, }, } satisfies AstroUserConfig & { server: { open: boolean } }; @@ -493,6 +494,7 @@ export const AstroConfigSchema = z.object({ .union([z.boolean(), z.custom((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 diff --git a/packages/astro/src/types/public/config.ts b/packages/astro/src/types/public/config.ts index 47514027e4ed..df0d0a551feb 100644 --- a/packages/astro/src/types/public/config.ts +++ b/packages/astro/src/types/public/config.ts @@ -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; }; } diff --git a/packages/astro/src/vite-plugin-astro/compile-rs.ts b/packages/astro/src/vite-plugin-astro/compile-rs.ts new file mode 100644 index 000000000000..9eff8a3916e5 --- /dev/null +++ b/packages/astro/src/vite-plugin-astro/compile-rs.ts @@ -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; +} + +export interface CompileAstroResult extends Omit { + map: SourceMapInput; +} + +export async function compileAstro({ + compileProps, + astroFileToCompileMetadata, +}: CompileAstroOption): Promise { + 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, + }; +} diff --git a/packages/astro/src/vite-plugin-astro/compile.ts b/packages/astro/src/vite-plugin-astro/compile.ts index 5000b42b70d7..0d8b000b29d6 100644 --- a/packages/astro/src/vite-plugin-astro/compile.ts +++ b/packages/astro/src/vite-plugin-astro/compile.ts @@ -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; @@ -13,7 +14,7 @@ interface CompileAstroOption { } export interface CompileAstroResult extends Omit { - map: ESBuildTransformResult['map']; + map: SourceMapInput; } interface EnhanceCompilerErrorOptions { diff --git a/packages/astro/src/vite-plugin-astro/index.ts b/packages/astro/src/vite-plugin-astro/index.ts index 0e302c10fdd1..030dda2af0bb 100644 --- a/packages/astro/src/vite-plugin-astro/index.ts +++ b/packages/astro/src/vite-plugin-astro/index.ts @@ -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'; @@ -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, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f262bb3308db..1bca68ba823a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -674,6 +674,9 @@ importers: '@astrojs/check': specifier: workspace:* version: link:../language-tools/astro-check + '@astrojs/compiler-rs': + specifier: ^0.1.0 + version: 0.1.0 '@playwright/test': specifier: 1.58.2 version: 1.58.2 @@ -7125,6 +7128,70 @@ packages: resolution: {integrity: sha512-bVzyKzEpIwqjihBU/aUzt1LQckJuHK0agd3/ITdXhPUYculrc6K1/K7H+XG4rwjXtg+ikT3PM05V1MVYWiIvQw==} engines: {node: '>=18.14.1'} + '@astrojs/compiler-binding-darwin-arm64@0.1.0': + resolution: {integrity: sha512-1uv15N1LZt3ss+y3VOIKCSicNaoOomb5D7VQuKxL4/QG1oier9ObPfIuxqTBxgtNUOPC87fYqiDkXRGxCi43Bg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@astrojs/compiler-binding-darwin-x64@0.1.0': + resolution: {integrity: sha512-I4VE7nwODs2UPf430EtQDwrAgJmZsdlxvIK/JqRvpu6C2nmjwGT4CibO8KACn+iBpUL4Bxk3+Ax+p3tVwKc9Ww==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@astrojs/compiler-binding-linux-arm64-gnu@0.1.0': + resolution: {integrity: sha512-VNorBIxOrT55hxSS1gdPYbM1RJHYL4ahOk15qINiwWFQPBb03vb5fY3zxUJrIXLdumb7Xvwt7TnrrGb6klFZbA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@astrojs/compiler-binding-linux-arm64-musl@0.1.0': + resolution: {integrity: sha512-VJUzo4vu6wY6ZkbDPofbzH4SyoinZGZzBCkZ75dGVZO1Z49J0fFb0NwRGd4VhGyrB6vBSa7fQTqZD04S2BYl2Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@astrojs/compiler-binding-linux-x64-gnu@0.1.0': + resolution: {integrity: sha512-GxCimSZcsG+gOhMH2kZGCUuUxZ2TxPHny7phIgjmdyE1iAY/zOKWl+uHWF/QazFsug0Iba4WlvjDOrVUL5HTDA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@astrojs/compiler-binding-linux-x64-musl@0.1.0': + resolution: {integrity: sha512-cR1B9rmMmmOoD4APlGQ3ujkOQQrDKpOY0GsLexwbrgzprkVW+mvEqRfCwoi9lJO7xTWMlIF5goesEDjvMvmnxg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@astrojs/compiler-binding-wasm32-wasi@0.1.0': + resolution: {integrity: sha512-gy9SVv+GTlBfeO6AOrhaLUYUkuKCvjFr94jiMI15DBS/xIXkxm5CMut4Y7yq7GEB5xMByMUOb6a0Yk9ELu8aDQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@astrojs/compiler-binding-win32-arm64-msvc@0.1.0': + resolution: {integrity: sha512-4lJSUe5qx+szLvly9FqtJ2BdAG5XEgaTk3anoq+JoaANA+pGk2mGGa50dTVcqzplVnqEQGY6+9dIklk/4hJ4IQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@astrojs/compiler-binding-win32-x64-msvc@0.1.0': + resolution: {integrity: sha512-HVpiVsr3Wcy1/1GMfzwSc0oFRpJw9Mz7PJpI53/xBt/0ySPWDmL0FZXRHSQgEvAfTcVmh2P2LPivK5AkEDKuzg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@astrojs/compiler-binding@0.1.0': + resolution: {integrity: sha512-NKI0m+2zA8n5s4Vg9rrzOIqJsKHsVu+ASVxaCk2PNPksdDBiPIZC0Hq2/kqJfzwCaGnU0kplcF/rM6kYlGpx/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@astrojs/compiler-rs@0.1.0': + resolution: {integrity: sha512-/ctF92BSNHLySrQgwa7z8U1SveNAsrZkumBybnoDmmtl1+ZV2p2ME7ODzk56xhidpwnP81V3VhPjWM8kWwq8fw==} + '@astrojs/compiler@2.13.1': resolution: {integrity: sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg==} @@ -16366,6 +16433,51 @@ snapshots: log-update: 5.0.1 sisteransi: 1.0.5 + '@astrojs/compiler-binding-darwin-arm64@0.1.0': + optional: true + + '@astrojs/compiler-binding-darwin-x64@0.1.0': + optional: true + + '@astrojs/compiler-binding-linux-arm64-gnu@0.1.0': + optional: true + + '@astrojs/compiler-binding-linux-arm64-musl@0.1.0': + optional: true + + '@astrojs/compiler-binding-linux-x64-gnu@0.1.0': + optional: true + + '@astrojs/compiler-binding-linux-x64-musl@0.1.0': + optional: true + + '@astrojs/compiler-binding-wasm32-wasi@0.1.0': + dependencies: + '@napi-rs/wasm-runtime': 1.1.1 + optional: true + + '@astrojs/compiler-binding-win32-arm64-msvc@0.1.0': + optional: true + + '@astrojs/compiler-binding-win32-x64-msvc@0.1.0': + optional: true + + '@astrojs/compiler-binding@0.1.0': + optionalDependencies: + '@astrojs/compiler-binding-darwin-arm64': 0.1.0 + '@astrojs/compiler-binding-darwin-x64': 0.1.0 + '@astrojs/compiler-binding-linux-arm64-gnu': 0.1.0 + '@astrojs/compiler-binding-linux-arm64-musl': 0.1.0 + '@astrojs/compiler-binding-linux-x64-gnu': 0.1.0 + '@astrojs/compiler-binding-linux-x64-musl': 0.1.0 + '@astrojs/compiler-binding-wasm32-wasi': 0.1.0 + '@astrojs/compiler-binding-win32-arm64-msvc': 0.1.0 + '@astrojs/compiler-binding-win32-x64-msvc': 0.1.0 + + '@astrojs/compiler-rs@0.1.0': + dependencies: + '@astrojs/compiler-binding': 0.1.0 + '@astrojs/compiler@2.13.1': {} '@astrojs/compiler@3.0.0-beta.0': {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f0fc2f493785..7145c214a76e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -32,6 +32,8 @@ minimumReleaseAge: 4320 minimumReleaseAgeExclude: # TODO: remove once more stable - '@flue/*' + - '@astrojs/compiler-rs' + - '@astrojs/compiler-binding*' peerDependencyRules: allowAny: - 'astro'