feat: restart dev server when tsconfig and tailwind config changes (#4947)
* First run * Works with tailwind! * Added TSConfig to watchlist * Changeset * Fix eslint * Renamed `isConfigReload` --> `isRestart` and `injectWatchTarget` --> `addWatchFile` * Refactored watchTargets to watchFiles * Refactor createSettings * addWatchFile now accepts URL * Fix getViteConfig * Expanded description of the change Co-authored-by: Matthew Phillips <matthew@skypack.dev>
This commit is contained in:
parent
5412c0c114
commit
a5e3ecc803
11 changed files with 129 additions and 74 deletions
6
.changeset/new-hotels-unite.md
Normal file
6
.changeset/new-hotels-unite.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
'astro': minor
|
||||
---
|
||||
|
||||
- Added `isRestart` and `addWatchFile` to integration step `isRestart`.
|
||||
- Restart dev server automatically when tsconfig changes.
|
7
.changeset/ten-candles-relate.md
Normal file
7
.changeset/ten-candles-relate.md
Normal file
|
@ -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.
|
|
@ -891,6 +891,7 @@ export interface AstroSettings {
|
|||
}[];
|
||||
tsConfig: TsConfigJson | undefined;
|
||||
tsConfigPath: string | undefined;
|
||||
watchFiles: string[];
|
||||
}
|
||||
|
||||
export type AsyncRendererComponentFn<U> = (
|
||||
|
@ -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.
|
||||
|
|
|
@ -9,7 +9,6 @@ import add from '../core/add/index.js';
|
|||
import build from '../core/build/index.js';
|
||||
import {
|
||||
createSettings,
|
||||
loadTSConfig,
|
||||
openConfig,
|
||||
resolveConfigPath,
|
||||
resolveFlags,
|
||||
|
@ -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
|
||||
|
@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ export function getViteConfig(inlineConfig: UserConfig) {
|
|||
const [
|
||||
{ mergeConfig },
|
||||
{ nodeLogDestination },
|
||||
{ openConfig, createSettings, loadTSConfig },
|
||||
{ openConfig, createSettings },
|
||||
{ createVite },
|
||||
{ runHookConfigSetup, runHookConfigDone },
|
||||
] = await Promise.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(
|
||||
{
|
||||
|
|
|
@ -135,7 +135,7 @@ interface LoadConfigOptions {
|
|||
validate?: boolean;
|
||||
logging: LogOptions;
|
||||
/** Invalidate when reloading a previously loaded config */
|
||||
isConfigReload?: boolean;
|
||||
isRestart?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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(
|
||||
|
|
|
@ -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] : [],
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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.`);
|
||||
|
@ -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[])[]) {
|
||||
|
|
|
@ -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';
|
||||
|
@ -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');
|
||||
|
|
|
@ -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';
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 =
|
||||
|
@ -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(
|
||||
|
@ -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));
|
||||
|
|
Loading…
Reference in a new issue