Use Vite to load non JS astro configs (#5377)

* Use Vite to load non JS astro configs

* Adding a changeset

* Allow config to not exist

* Use a file url

* Use proload as a fallback

* add missing peerdep

* fix lint mistakes

* Refactor the vite-load

* First check if the file exists

* Pass through fs

* Update packages/astro/src/core/config/vite-load.ts

Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>

* Also load astro.config.cjs

* Do search before trying to load

Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>
This commit is contained in:
Matthew Phillips 2022-11-15 10:02:23 -05:00 committed by GitHub
parent 095de72a17
commit 40226dd14d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 242 additions and 66 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Uses vite to load astro.config.ts files

View file

@ -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",

View file

@ -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,

View file

@ -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) {

View file

@ -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<LoadConfigOptions, 'cwd' | 'flags'>
configOptions: Pick<LoadConfigOptions, 'cwd' | 'flags'> & { fs: typeof fs }
): Promise<string | undefined> {
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<Open
const flags = resolveFlags(configOptions.flags || {});
let userConfig: AstroUserConfig = {};
const config = await tryLoadConfig(configOptions, flags, root);
const config = await tryLoadConfig(configOptions, root);
if (config) {
userConfig = config.value;
}
@ -216,7 +212,6 @@ interface TryLoadConfigResult {
async function tryLoadConfig(
configOptions: LoadConfigOptions,
flags: CLIFlags,
root: string
): Promise<TryLoadConfigResult | undefined> {
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<Astr
const flags = resolveFlags(configOptions.flags || {});
let userConfig: AstroUserConfig = {};
const config = await tryLoadConfig(configOptions, flags, root);
const config = await tryLoadConfig(configOptions, root);
if (config) {
userConfig = config.value;
}

View file

@ -0,0 +1,138 @@
import * as vite from 'vite';
import npath from 'path';
import { pathToFileURL } from 'url';
import type fsType from 'fs';
import { AstroError, AstroErrorData } from '../errors/index.js';
// Fallback for legacy
import load from '@proload/core';
import loadTypeScript from '@proload/plugin-tsm';
load.use([loadTypeScript]);
export interface ViteLoader {
root: string;
viteServer: vite.ViteDevServer;
}
async function createViteLoader(root: string): Promise<ViteLoader> {
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<boolean> {
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<string, any>;
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();
}
}
}

View file

@ -0,0 +1,7 @@
import { defineConfig } from 'astro/config';
import tailwind from "@astrojs/tailwind";
// https://astro.build/config
export default defineConfig({
integrations: [tailwind()]
});

View file

@ -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"
}
}

View file

@ -0,0 +1,4 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['src/**/*.{astro,tsx}']
};

View file

@ -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();
}
});
});

View file

@ -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({

View file

@ -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:*