diff --git a/.changeset/swift-bees-hope.md b/.changeset/swift-bees-hope.md new file mode 100644 index 000000000..0bf096e3a --- /dev/null +++ b/.changeset/swift-bees-hope.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Uses vite to load astro.config.ts files diff --git a/packages/astro/package.json b/packages/astro/package.json index 279472c6e..13a82395d 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -111,11 +111,11 @@ "@babel/plugin-transform-react-jsx": "^7.17.12", "@babel/traverse": "^7.18.2", "@babel/types": "^7.18.4", - "@proload/core": "^0.3.3", - "@proload/plugin-tsm": "^0.2.1", "@types/babel__core": "^7.1.19", "@types/html-escaper": "^3.0.0", "@types/yargs-parser": "^21.0.0", + "@proload/core": "^0.3.3", + "@proload/plugin-tsm": "^0.2.1", "boxen": "^6.2.1", "ci-info": "^3.3.1", "common-ancestor-path": "^1.0.1", diff --git a/packages/astro/src/cli/index.ts b/packages/astro/src/cli/index.ts index ddd4acc83..33457a6a1 100644 --- a/packages/astro/src/cli/index.ts +++ b/packages/astro/src/cli/index.ts @@ -3,6 +3,7 @@ import * as colors from 'kleur/colors'; import type { Arguments as Flags } from 'yargs-parser'; import yargs from 'yargs-parser'; import { z } from 'zod'; +import fs from 'fs'; import { createSettings, openConfig, @@ -88,7 +89,7 @@ async function handleConfigError( e: any, { cwd, flags, logging }: { cwd?: string; flags?: Flags; logging: LogOptions } ) { - const path = await resolveConfigPath({ cwd, flags }); + const path = await resolveConfigPath({ cwd, flags, fs }); if (e instanceof Error) { if (path) { error(logging, 'astro', `Unable to load ${colors.bold(path)}\n`); @@ -173,7 +174,7 @@ async function runCommand(cmd: string, flags: yargs.Arguments) { const { default: devServer } = await import('../core/dev/index.js'); const configFlag = resolveFlags(flags).config; - const configFlagPath = configFlag ? await resolveConfigPath({ cwd: root, flags }) : undefined; + const configFlagPath = configFlag ? await resolveConfigPath({ cwd: root, flags, fs }) : undefined; await devServer(settings, { configFlag, diff --git a/packages/astro/src/core/add/index.ts b/packages/astro/src/core/add/index.ts index 3c2d20a87..536cbe2f9 100644 --- a/packages/astro/src/core/add/index.ts +++ b/packages/astro/src/core/add/index.ts @@ -2,7 +2,7 @@ import type { AstroTelemetry } from '@astrojs/telemetry'; import boxen from 'boxen'; import { diffWords } from 'diff'; import { execa } from 'execa'; -import { existsSync, promises as fs } from 'fs'; +import fsMod, { existsSync, promises as fs } from 'fs'; import { bold, cyan, dim, green, magenta, red, yellow } from 'kleur/colors'; import ora from 'ora'; import path from 'path'; @@ -164,7 +164,7 @@ export default async function add(names: string[], { cwd, flags, logging, teleme } } - const rawConfigPath = await resolveConfigPath({ cwd, flags }); + const rawConfigPath = await resolveConfigPath({ cwd, flags, fs: fsMod }); let configURL = rawConfigPath ? pathToFileURL(rawConfigPath) : undefined; if (configURL) { diff --git a/packages/astro/src/core/config/config.ts b/packages/astro/src/core/config/config.ts index 13c87fa9a..c17ec6a39 100644 --- a/packages/astro/src/core/config/config.ts +++ b/packages/astro/src/core/config/config.ts @@ -1,20 +1,16 @@ import type { Arguments as Flags } from 'yargs-parser'; import type { AstroConfig, AstroUserConfig, CLIFlags } from '../../@types/astro'; -import load, { ProloadError, resolve } from '@proload/core'; -import loadTypeScript from '@proload/plugin-tsm'; import fs from 'fs'; import * as colors from 'kleur/colors'; import path from 'path'; import { fileURLToPath, pathToFileURL } from 'url'; -import * as vite from 'vite'; import { mergeConfig as mergeViteConfig } from 'vite'; import { AstroError, AstroErrorData } from '../errors/index.js'; import { LogOptions } from '../logger/core.js'; import { arraify, isObject, isURL } from '../util.js'; import { createRelativeSchema } from './schema.js'; - -load.use([loadTypeScript]); +import { loadConfigWithVite } from './vite-load.js'; export const LEGACY_ASTRO_CONFIG_KEYS = new Set([ 'projectRoot', @@ -151,7 +147,7 @@ interface LoadConfigOptions { * instead of the resolved config */ export async function resolveConfigPath( - configOptions: Pick + configOptions: Pick & { fs: typeof fs } ): Promise { const root = resolveRoot(configOptions.cwd); const flags = resolveFlags(configOptions.flags || {}); @@ -165,14 +161,14 @@ export async function resolveConfigPath( // Resolve config file path using Proload // If `userConfigPath` is `undefined`, Proload will search for `astro.config.[cm]?[jt]s` try { - const configPath = await resolve('astro', { - mustExist: !!userConfigPath, - cwd: root, - filePath: userConfigPath, + const config = await loadConfigWithVite({ + configPath: userConfigPath, + root, + fs: configOptions.fs }); - return configPath; + return config.filePath; } catch (e) { - if (e instanceof ProloadError && flags.config) { + if (flags.config) { throw new AstroError({ ...AstroErrorData.ConfigNotFound, message: AstroErrorData.ConfigNotFound.message(flags.config), @@ -195,7 +191,7 @@ export async function openConfig(configOptions: LoadConfigOptions): Promise { const fsMod = configOptions.fsMod ?? fs; @@ -225,6 +220,7 @@ async function tryLoadConfig( let configPath = await resolveConfigPath({ cwd: configOptions.cwd, flags: configOptions.flags, + fs: fsMod }); if (!configPath) return undefined; if (configOptions.isRestart) { @@ -246,52 +242,14 @@ async function tryLoadConfig( }; configPath = tempConfigPath; } - - const config = await load('astro', { - mustExist: !!configPath, - cwd: root, - filePath: configPath, + + // Create a vite server to load the config + const config = await loadConfigWithVite({ + configPath, + fs: fsMod, + root }); - return config as TryLoadConfigResult; - } catch (e) { - if (e instanceof ProloadError && flags.config) { - throw new AstroError({ - ...AstroErrorData.ConfigNotFound, - message: AstroErrorData.ConfigNotFound.message(flags.config), - }); - } - - const configPath = await resolveConfigPath(configOptions); - if (!configPath) { - throw e; - } - - // Fallback to use Vite DevServer - const viteServer = await vite.createServer({ - server: { middlewareMode: true, hmr: false }, - optimizeDeps: { entries: [] }, - clearScreen: false, - appType: 'custom', - // NOTE: Vite doesn't externalize linked packages by default. During testing locally, - // these dependencies trip up Vite's dev SSR transform. In the future, we should - // avoid `vite.createServer` and use `loadConfigFromFile` instead. - ssr: { - external: ['@astrojs/mdx', '@astrojs/react'], - }, - }); - try { - const mod = await viteServer.ssrLoadModule(configPath); - - if (mod?.default) { - return { - value: mod.default, - filePath: configPath, - }; - } - } finally { - await viteServer.close(); - } } finally { await finallyCleanup(); } @@ -306,7 +264,7 @@ export async function loadConfig(configOptions: LoadConfigOptions): Promise { + const viteServer = await vite.createServer({ + server: { middlewareMode: true, hmr: false }, + optimizeDeps: { entries: [] }, + clearScreen: false, + appType: 'custom', + ssr: { + // NOTE: Vite doesn't externalize linked packages by default. During testing locally, + // these dependencies trip up Vite's dev SSR transform. In the future, we should + // avoid `vite.createServer` and use `loadConfigFromFile` instead. + external: ['@astrojs/tailwind', '@astrojs/mdx', '@astrojs/react'] + } + }); + + return { + root, + viteServer, + }; +} + +async function stat(fs: typeof fsType, configPath: string, mustExist: boolean): Promise { + try { + await fs.promises.stat(configPath); + return true; + } catch { + if(mustExist) { + throw new AstroError({ + ...AstroErrorData.ConfigNotFound, + message: AstroErrorData.ConfigNotFound.message(configPath), + }); + } + return false; + } +} + +async function search(fs: typeof fsType, root: string) { + const paths = [ + 'astro.config.mjs', + 'astro.config.js', + 'astro.config.ts', + 'astro.config.mts', + 'astro.config.cjs', + 'astro.config.cjs' + ].map(path => npath.join(root, path)); + + for(const file of paths) { + // First verify the file event exists + const exists = await stat(fs, file, false); + if(exists) { + return file; + } + } +} + +interface LoadConfigWithViteOptions { + root: string; + configPath: string | undefined; + fs: typeof fsType; +} + +export async function loadConfigWithVite({ configPath, fs, root }: LoadConfigWithViteOptions): Promise<{ + value: Record; + filePath?: string; +}> { + let file: string; + if(configPath) { + // Go ahead and check if the file exists and throw if not. + await stat(fs, configPath, true); + file = configPath; + } else { + const found = await search(fs, root); + if(!found) { + // No config file found, return an empty config that will be populated with defaults + return { + value: {}, + filePath: undefined + }; + } else { + file = found; + } + } + + + // Try loading with Node import() + if(/\.[cm]?js$/.test(file)) { + const config = await import(pathToFileURL(file).toString()); + return { + value: config.default ?? {}, + filePath: file + }; + } + + // Try Loading with Vite + let loader: ViteLoader | undefined; + try { + loader = await createViteLoader(root); + const mod = await loader.viteServer.ssrLoadModule(file); + return { + value: mod.default ?? {}, + filePath: file + } + } catch { + + // Try loading with Proload + // TODO deprecate - this is only for legacy compatibility + const res = await load('astro', { + mustExist: true, + cwd: root, + filePath: file, + }); + return { + value: res?.value ?? {}, + filePath: file + }; + + } finally { + if(loader) { + await loader.viteServer.close(); + } + } +} diff --git a/packages/astro/test/fixtures/tailwindcss-ts/astro.config.ts b/packages/astro/test/fixtures/tailwindcss-ts/astro.config.ts new file mode 100644 index 000000000..0a5f36a87 --- /dev/null +++ b/packages/astro/test/fixtures/tailwindcss-ts/astro.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'astro/config'; +import tailwind from "@astrojs/tailwind"; + +// https://astro.build/config +export default defineConfig({ + integrations: [tailwind()] +}); \ No newline at end of file diff --git a/packages/astro/test/fixtures/tailwindcss-ts/package.json b/packages/astro/test/fixtures/tailwindcss-ts/package.json new file mode 100644 index 000000000..78ead447f --- /dev/null +++ b/packages/astro/test/fixtures/tailwindcss-ts/package.json @@ -0,0 +1,11 @@ +{ + "name": "@test/tailwindcss-ts", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/tailwind": "workspace:*", + "astro": "workspace:*", + "tailwindcss": "^3.2.4", + "postcss": ">=8.3.3 <9.0.0" + } +} diff --git a/packages/astro/test/fixtures/tailwindcss-ts/tailwind.config.cjs b/packages/astro/test/fixtures/tailwindcss-ts/tailwind.config.cjs new file mode 100644 index 000000000..f4033a222 --- /dev/null +++ b/packages/astro/test/fixtures/tailwindcss-ts/tailwind.config.cjs @@ -0,0 +1,4 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['src/**/*.{astro,tsx}'] +}; \ No newline at end of file diff --git a/packages/astro/test/units/dev/restart.test.js b/packages/astro/test/units/dev/restart.test.js index 2e78f789d..96c60c608 100644 --- a/packages/astro/test/units/dev/restart.test.js +++ b/packages/astro/test/units/dev/restart.test.js @@ -7,6 +7,8 @@ import { startContainer, } from '../../../dist/core/dev/index.js'; import { createFs, createRequestAndResponse, triggerFSEvent } from '../test-utils.js'; +import { createSettings, openConfig } from '../../../dist/core/config/index.js'; +import { defaultLogging } from '../../test-utils.js'; const root = new URL('../../fixtures/alias/', import.meta.url); @@ -109,4 +111,40 @@ describe('dev container restarts', () => { await restart.container.close(); } }); + + it('Is able to restart project using Tailwind + astro.config.ts', async () => { + const troot = new URL('../../fixtures/tailwindcss-ts/', import.meta.url); + const fs = createFs( + { + '/src/pages/index.astro': ``, + '/astro.config.ts': ``, + }, + troot + ); + + const { astroConfig } = await openConfig({ + cwd: troot, + flags: {}, + cmd: 'dev', + logging: defaultLogging, + }); + const settings = createSettings(astroConfig); + + let restart = await createContainerWithAutomaticRestart({ + params: { fs, root, settings }, + }); + await startContainer(restart.container); + expect(isStarted(restart.container)).to.equal(true); + + try { + // Trigger a change + let restartComplete = restart.restarted(); + triggerFSEvent(restart.container, fs, '/astro.config.ts', 'change'); + await restartComplete; + + expect(isStarted(restart.container)).to.equal(true); + } finally { + await restart.container.close(); + } + }); }); diff --git a/packages/integrations/image/src/index.ts b/packages/integrations/image/src/index.ts index ebb91e17b..43f64935b 100644 --- a/packages/integrations/image/src/index.ts +++ b/packages/integrations/image/src/index.ts @@ -82,7 +82,9 @@ export default function integration(options: IntegrationOptions = {}): AstroInte 'astro:config:setup': async ({ command, config, updateConfig, injectRoute }) => { needsBuildConfig = !config.build?.server; _config = config; - updateConfig({ vite: getViteConfiguration(command === 'dev') }); + updateConfig({ + vite: getViteConfiguration(command === 'dev'), + }); if (command === 'dev' || config.output === 'server') { injectRoute({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b4f3a0ac2..c6797e1e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2390,6 +2390,18 @@ importers: postcss: 8.4.19 tailwindcss: 3.2.4_postcss@8.4.19 + packages/astro/test/fixtures/tailwindcss-ts: + specifiers: + '@astrojs/tailwind': workspace:* + astro: workspace:* + postcss: '>=8.3.3 <9.0.0' + tailwindcss: ^3.2.4 + dependencies: + '@astrojs/tailwind': link:../../../../integrations/tailwind + astro: link:../../.. + postcss: 8.4.19 + tailwindcss: 3.2.4_postcss@8.4.19 + packages/astro/test/fixtures/third-party-astro: specifiers: astro: workspace:*