Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: restart dev server when tsconfig and tailwind config changes #4947

Merged
merged 14 commits into from
Oct 12, 2022
6 changes: 6 additions & 0 deletions .changeset/new-hotels-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'astro': minor
---

- Added `isRestart` and `addWatchFile` to integration step `isRestart`.
- Restart dev server automatically when tsconfig changes.
7 changes: 7 additions & 0 deletions .changeset/ten-candles-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astrojs/tailwind': minor
---

## HMR on config file changes

New in this release is the ability for config changes to automatically reflect via HMR. Now when you edit your `tsconfig.json` or `tailwind.config.js` configs, the changes will reload automatically without the need to restart your dev server.
3 changes: 3 additions & 0 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,7 @@ export interface AstroSettings {
}[];
tsConfig: TsConfigJson | undefined;
tsConfigPath: string | undefined;
watchFiles: string[];
}

export type AsyncRendererComponentFn<U> = (
Expand Down Expand Up @@ -1142,8 +1143,10 @@ export interface AstroIntegration {
'astro:config:setup'?: (options: {
config: AstroConfig;
command: 'dev' | 'build';
isRestart: boolean;
updateConfig: (newConfig: Record<string, any>) => void;
addRenderer: (renderer: AstroRenderer) => void;
addWatchFile: (path: URL | string) => void;
injectScript: (stage: InjectedScriptStage, content: string) => void;
injectRoute: (injectRoute: InjectedRoute) => void;
// TODO: Add support for `injectElement()` for full HTML element injection, not just scripts.
Expand Down
86 changes: 43 additions & 43 deletions packages/astro/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import add from '../core/add/index.js';
import build from '../core/build/index.js';
import {
createSettings,
loadTSConfig,
openConfig,
resolveConfigPath,
resolveFlags,
Expand Down Expand Up @@ -168,12 +167,7 @@ async function runCommand(cmd: string, flags: yargs.Arguments) {
});
if (!initialAstroConfig) return;
telemetry.record(event.eventCliSession(cmd, initialUserConfig, flags));
let initialTsConfig = loadTSConfig(root);
let settings = createSettings({
config: initialAstroConfig,
tsConfig: initialTsConfig?.config,
tsConfigPath: initialTsConfig?.path,
});
let settings = createSettings(initialAstroConfig, root);

// Common CLI Commands:
// These commands run normally. All commands are assumed to have been handled
Expand All @@ -191,42 +185,48 @@ async function runCommand(cmd: string, flags: yargs.Arguments) {

const handleServerRestart = (logMsg: string) =>
async function (changedFile: string) {
if (
!restartInFlight &&
(configFlag
? // If --config is specified, only watch changes for this file
configFlagPath && normalizePath(configFlagPath) === normalizePath(changedFile)
: // Otherwise, watch for any astro.config.* file changes in project root
new RegExp(
`${normalizePath(resolvedRoot)}.*astro\.config\.((mjs)|(cjs)|(js)|(ts))$`
).test(normalizePath(changedFile)))
) {
restartInFlight = true;
console.clear();
try {
const newConfig = await openConfig({
cwd: root,
flags,
cmd,
logging,
isConfigReload: true,
});
info(logging, 'astro', logMsg + '\n');
let astroConfig = newConfig.astroConfig;
let tsconfig = loadTSConfig(root);
settings = createSettings({
config: astroConfig,
tsConfig: tsconfig?.config,
tsConfigPath: tsconfig?.path,
});
await stop();
await startDevServer({ isRestart: true });
} catch (e) {
await handleConfigError(e, { cwd: root, flags, logging });
await stop();
info(logging, 'astro', 'Continuing with previous valid configuration\n');
await startDevServer({ isRestart: true });
}
if (restartInFlight) return;

let shouldRestart = false;

// If the config file changed, reload the config and restart the server.
shouldRestart = configFlag
? // If --config is specified, only watch changes for this file
!!configFlagPath && normalizePath(configFlagPath) === normalizePath(changedFile)
: // Otherwise, watch for any astro.config.* file changes in project root
new RegExp(
`${normalizePath(resolvedRoot)}.*astro\.config\.((mjs)|(cjs)|(js)|(ts))$`
).test(normalizePath(changedFile));

if (!shouldRestart && settings.watchFiles.length > 0) {
// If the config file didn't change, check if any of the watched files changed.
shouldRestart = settings.watchFiles.some(
(path) => normalizePath(path) === normalizePath(changedFile)
);
}

if (!shouldRestart) return;

restartInFlight = true;
console.clear();
try {
const newConfig = await openConfig({
cwd: root,
flags,
cmd,
logging,
isRestart: true,
});
info(logging, 'astro', logMsg + '\n');
let astroConfig = newConfig.astroConfig;
settings = createSettings(astroConfig, root);
await stop();
await startDevServer({ isRestart: true });
} catch (e) {
await handleConfigError(e, { cwd: root, flags, logging });
await stop();
info(logging, 'astro', 'Continuing with previous valid configuration\n');
await startDevServer({ isRestart: true });
}
};

Expand Down
9 changes: 2 additions & 7 deletions packages/astro/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function getViteConfig(inlineConfig: UserConfig) {
const [
{ mergeConfig },
{ nodeLogDestination },
{ openConfig, createSettings, loadTSConfig },
{ openConfig, createSettings },
{ createVite },
{ runHookConfigSetup, runHookConfigDone },
] = await Promise.all([
Expand All @@ -34,12 +34,7 @@ export function getViteConfig(inlineConfig: UserConfig) {
cmd,
logging,
});
const initialTsConfig = loadTSConfig(inlineConfig.root);
const settings = createSettings({
config,
tsConfig: initialTsConfig?.config,
tsConfigPath: initialTsConfig?.path,
});
const settings = createSettings(config, inlineConfig.root);
await runHookConfigSetup({ settings, command: cmd, logging });
const viteConfig = await createVite(
{
Expand Down
4 changes: 2 additions & 2 deletions packages/astro/src/core/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ interface LoadConfigOptions {
validate?: boolean;
logging: LogOptions;
/** Invalidate when reloading a previously loaded config */
isConfigReload?: boolean;
isRestart?: boolean;
}

/**
Expand Down Expand Up @@ -222,7 +222,7 @@ async function tryLoadConfig(
flags: configOptions.flags,
});
if (!configPath) return undefined;
if (configOptions.isConfigReload) {
if (configOptions.isRestart) {
// Hack: Write config to temporary file at project root
// This invalidates and reloads file contents when using ESM imports or "resolve"
const tempConfigPath = path.join(
Expand Down
15 changes: 6 additions & 9 deletions packages/astro/src/core/config/settings.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
import type { TsConfigJson } from 'tsconfig-resolver';
import type { AstroConfig, AstroSettings } from '../../@types/astro';

import jsxRenderer from '../../jsx/renderer.js';
import { loadTSConfig } from './tsconfig.js';

export interface CreateSettings {
config: AstroConfig;
tsConfig?: TsConfigJson;
tsConfigPath?: string;
}
export function createSettings(config: AstroConfig, cwd?: string): AstroSettings {
const tsconfig = loadTSConfig(cwd);

export function createSettings({ config, tsConfig, tsConfigPath }: CreateSettings): AstroSettings {
return {
config,
tsConfig,
tsConfigPath,
tsConfig: tsconfig?.config,
tsConfigPath: tsconfig?.path,

adapter: undefined,
injectedRoutes: [],
pageExtensions: ['.astro', '.md', '.html'],
renderers: [jsxRenderer],
scripts: [],
watchFiles: tsconfig?.exists ? [tsconfig.path, ...tsconfig.extendedPaths] : [],
};
}
7 changes: 6 additions & 1 deletion packages/astro/src/core/dev/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ export default async function dev(
const devStart = performance.now();
applyPolyfill();
await options.telemetry.record([]);
settings = await runHookConfigSetup({ settings, command: 'dev', logging: options.logging });
settings = await runHookConfigSetup({
settings,
command: 'dev',
logging: options.logging,
isRestart: options.isRestart,
});
const { host, port } = settings.config.server;
const { isRestart = false } = options;

Expand Down
7 changes: 7 additions & 0 deletions packages/astro/src/integrations/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { bold } from 'kleur/colors';
import type { AddressInfo } from 'net';
import { fileURLToPath } from 'node:url';
import type { InlineConfig, ViteDevServer } from 'vite';
import {
AstroConfig,
Expand Down Expand Up @@ -37,10 +38,12 @@ export async function runHookConfigSetup({
settings,
command,
logging,
isRestart = false,
}: {
settings: AstroSettings;
command: 'dev' | 'build';
logging: LogOptions;
isRestart?: boolean;
}): Promise<AstroSettings> {
// An adapter is an integration, so if one is provided push it.
if (settings.config.adapter) {
Expand All @@ -66,6 +69,7 @@ export async function runHookConfigSetup({
const hooks: HookParameters<'astro:config:setup'> = {
config: updatedConfig,
command,
isRestart,
addRenderer(renderer: AstroRenderer) {
if (!renderer.name) {
throw new Error(`Integration ${bold(integration.name)} has an unnamed renderer.`);
Expand All @@ -86,6 +90,9 @@ export async function runHookConfigSetup({
injectRoute: (injectRoute) => {
updatedSettings.injectedRoutes.push(injectRoute);
},
addWatchFile: (path) => {
updatedSettings.watchFiles.push(path instanceof URL ? fileURLToPath(path) : path);
},
};
// Semi-private `addPageExtension` hook
function addPageExtension(...input: (string | string[])[]) {
Expand Down
9 changes: 2 additions & 7 deletions packages/astro/test/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { polyfill } from '@astrojs/webapi';
import fs from 'fs';
import { fileURLToPath } from 'url';
import { loadConfig } from '../dist/core/config/config.js';
import { createSettings, loadTSConfig } from '../dist/core/config/index.js';
import { createSettings } from '../dist/core/config/index.js';
import dev from '../dist/core/dev/index.js';
import build from '../dist/core/build/index.js';
import preview from '../dist/core/preview/index.js';
Expand Down Expand Up @@ -95,12 +95,7 @@ export async function loadFixture(inlineConfig) {
if (inlineConfig.base && !inlineConfig.base.endsWith('/')) {
config.base = inlineConfig.base + '/';
}
let tsconfig = loadTSConfig(fileURLToPath(cwd));
let settings = createSettings({
config,
tsConfig: tsconfig?.config,
tsConfigPath: tsconfig?.path,
});
let settings = createSettings(config, fileURLToPath(cwd));
if (config.integrations.find((integration) => integration.name === '@astrojs/mdx')) {
// Enable default JSX integration. It needs to come first, so unshift rather than push!
const { default: jsxRenderer } = await import('astro/jsx/renderer.js');
Expand Down
50 changes: 45 additions & 5 deletions packages/integrations/tailwind/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import load from '@proload/core';
import load, { resolve } from '@proload/core';
import type { AstroIntegration } from 'astro';
import autoprefixerPlugin from 'autoprefixer';
import fs from 'fs/promises';
import path from 'path';
import tailwindPlugin, { Config as TailwindConfig } from 'tailwindcss';
import resolveConfig from 'tailwindcss/resolveConfig.js';
Expand All @@ -17,7 +18,7 @@ function getDefaultTailwindConfig(srcUrl: URL): TailwindConfig {
}) as TailwindConfig;
}

async function getUserConfig(root: URL, configPath?: string) {
async function getUserConfig(root: URL, configPath?: string, isRestart = false) {
const resolvedRoot = fileURLToPath(root);
let userConfigPath: string | undefined;

Expand All @@ -26,7 +27,42 @@ async function getUserConfig(root: URL, configPath?: string) {
userConfigPath = fileURLToPath(new URL(configPathWithLeadingSlash, root));
}

return await load('tailwind', { mustExist: false, cwd: resolvedRoot, filePath: userConfigPath });
if (isRestart) {
// Hack: Write config to temporary file at project root
// This invalidates and reloads file contents when using ESM imports or "resolve"
const resolvedConfigPath = (await resolve('tailwind', {
mustExist: false,
cwd: resolvedRoot,
filePath: userConfigPath,
})) as string;

const { dir, base } = path.parse(resolvedConfigPath);
const tempConfigPath = path.join(dir, `.temp.${Date.now()}.${base}`);
await fs.copyFile(resolvedConfigPath, tempConfigPath);

const result = await load('tailwind', {
mustExist: false,
cwd: resolvedRoot,
filePath: tempConfigPath,
});

try {
await fs.unlink(tempConfigPath);
} catch {
/** file already removed */
}

return {
...result,
filePath: resolvedConfigPath,
};
} else {
return await load('tailwind', {
mustExist: false,
cwd: resolvedRoot,
filePath: userConfigPath,
});
}
}

type TailwindOptions =
Expand Down Expand Up @@ -55,9 +91,9 @@ export default function tailwindIntegration(options?: TailwindOptions): AstroInt
return {
name: '@astrojs/tailwind',
hooks: {
'astro:config:setup': async ({ config, injectScript }) => {
'astro:config:setup': async ({ config, injectScript, addWatchFile, isRestart }) => {
// Inject the Tailwind postcss plugin
const userConfig = await getUserConfig(config.root, customConfigPath);
const userConfig = await getUserConfig(config.root, customConfigPath, isRestart);

if (customConfigPath && !userConfig?.value) {
throw new Error(
Expand All @@ -67,6 +103,10 @@ export default function tailwindIntegration(options?: TailwindOptions): AstroInt
);
}

if (userConfig?.filePath) {
addWatchFile(userConfig.filePath);
}

const tailwindConfig: TailwindConfig =
(userConfig?.value as TailwindConfig) ?? getDefaultTailwindConfig(config.srcDir);
config.style.postcss.plugins.push(tailwindPlugin(tailwindConfig));
Expand Down