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;
|
tsConfig: TsConfigJson | undefined;
|
||||||
tsConfigPath: string | undefined;
|
tsConfigPath: string | undefined;
|
||||||
|
watchFiles: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AsyncRendererComponentFn<U> = (
|
export type AsyncRendererComponentFn<U> = (
|
||||||
|
@ -1142,8 +1143,10 @@ export interface AstroIntegration {
|
||||||
'astro:config:setup'?: (options: {
|
'astro:config:setup'?: (options: {
|
||||||
config: AstroConfig;
|
config: AstroConfig;
|
||||||
command: 'dev' | 'build';
|
command: 'dev' | 'build';
|
||||||
|
isRestart: boolean;
|
||||||
updateConfig: (newConfig: Record<string, any>) => void;
|
updateConfig: (newConfig: Record<string, any>) => void;
|
||||||
addRenderer: (renderer: AstroRenderer) => void;
|
addRenderer: (renderer: AstroRenderer) => void;
|
||||||
|
addWatchFile: (path: URL | string) => void;
|
||||||
injectScript: (stage: InjectedScriptStage, content: string) => void;
|
injectScript: (stage: InjectedScriptStage, content: string) => void;
|
||||||
injectRoute: (injectRoute: InjectedRoute) => void;
|
injectRoute: (injectRoute: InjectedRoute) => void;
|
||||||
// TODO: Add support for `injectElement()` for full HTML element injection, not just scripts.
|
// 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 build from '../core/build/index.js';
|
||||||
import {
|
import {
|
||||||
createSettings,
|
createSettings,
|
||||||
loadTSConfig,
|
|
||||||
openConfig,
|
openConfig,
|
||||||
resolveConfigPath,
|
resolveConfigPath,
|
||||||
resolveFlags,
|
resolveFlags,
|
||||||
|
@ -168,12 +167,7 @@ async function runCommand(cmd: string, flags: yargs.Arguments) {
|
||||||
});
|
});
|
||||||
if (!initialAstroConfig) return;
|
if (!initialAstroConfig) return;
|
||||||
telemetry.record(event.eventCliSession(cmd, initialUserConfig, flags));
|
telemetry.record(event.eventCliSession(cmd, initialUserConfig, flags));
|
||||||
let initialTsConfig = loadTSConfig(root);
|
let settings = createSettings(initialAstroConfig, root);
|
||||||
let settings = createSettings({
|
|
||||||
config: initialAstroConfig,
|
|
||||||
tsConfig: initialTsConfig?.config,
|
|
||||||
tsConfigPath: initialTsConfig?.path,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Common CLI Commands:
|
// Common CLI Commands:
|
||||||
// These commands run normally. All commands are assumed to have been handled
|
// 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) =>
|
const handleServerRestart = (logMsg: string) =>
|
||||||
async function (changedFile: string) {
|
async function (changedFile: string) {
|
||||||
if (
|
if (restartInFlight) return;
|
||||||
!restartInFlight &&
|
|
||||||
(configFlag
|
let shouldRestart = false;
|
||||||
? // If --config is specified, only watch changes for this file
|
|
||||||
configFlagPath && normalizePath(configFlagPath) === normalizePath(changedFile)
|
// If the config file changed, reload the config and restart the server.
|
||||||
: // Otherwise, watch for any astro.config.* file changes in project root
|
shouldRestart = configFlag
|
||||||
new RegExp(
|
? // If --config is specified, only watch changes for this file
|
||||||
`${normalizePath(resolvedRoot)}.*astro\.config\.((mjs)|(cjs)|(js)|(ts))$`
|
!!configFlagPath && normalizePath(configFlagPath) === normalizePath(changedFile)
|
||||||
).test(normalizePath(changedFile)))
|
: // Otherwise, watch for any astro.config.* file changes in project root
|
||||||
) {
|
new RegExp(
|
||||||
restartInFlight = true;
|
`${normalizePath(resolvedRoot)}.*astro\.config\.((mjs)|(cjs)|(js)|(ts))$`
|
||||||
console.clear();
|
).test(normalizePath(changedFile));
|
||||||
try {
|
|
||||||
const newConfig = await openConfig({
|
if (!shouldRestart && settings.watchFiles.length > 0) {
|
||||||
cwd: root,
|
// If the config file didn't change, check if any of the watched files changed.
|
||||||
flags,
|
shouldRestart = settings.watchFiles.some(
|
||||||
cmd,
|
(path) => normalizePath(path) === normalizePath(changedFile)
|
||||||
logging,
|
);
|
||||||
isConfigReload: true,
|
}
|
||||||
});
|
|
||||||
info(logging, 'astro', logMsg + '\n');
|
if (!shouldRestart) return;
|
||||||
let astroConfig = newConfig.astroConfig;
|
|
||||||
let tsconfig = loadTSConfig(root);
|
restartInFlight = true;
|
||||||
settings = createSettings({
|
console.clear();
|
||||||
config: astroConfig,
|
try {
|
||||||
tsConfig: tsconfig?.config,
|
const newConfig = await openConfig({
|
||||||
tsConfigPath: tsconfig?.path,
|
cwd: root,
|
||||||
});
|
flags,
|
||||||
await stop();
|
cmd,
|
||||||
await startDevServer({ isRestart: true });
|
logging,
|
||||||
} catch (e) {
|
isRestart: true,
|
||||||
await handleConfigError(e, { cwd: root, flags, logging });
|
});
|
||||||
await stop();
|
info(logging, 'astro', logMsg + '\n');
|
||||||
info(logging, 'astro', 'Continuing with previous valid configuration\n');
|
let astroConfig = newConfig.astroConfig;
|
||||||
await startDevServer({ isRestart: true });
|
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 [
|
const [
|
||||||
{ mergeConfig },
|
{ mergeConfig },
|
||||||
{ nodeLogDestination },
|
{ nodeLogDestination },
|
||||||
{ openConfig, createSettings, loadTSConfig },
|
{ openConfig, createSettings },
|
||||||
{ createVite },
|
{ createVite },
|
||||||
{ runHookConfigSetup, runHookConfigDone },
|
{ runHookConfigSetup, runHookConfigDone },
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
|
@ -34,12 +34,7 @@ export function getViteConfig(inlineConfig: UserConfig) {
|
||||||
cmd,
|
cmd,
|
||||||
logging,
|
logging,
|
||||||
});
|
});
|
||||||
const initialTsConfig = loadTSConfig(inlineConfig.root);
|
const settings = createSettings(config, inlineConfig.root);
|
||||||
const settings = createSettings({
|
|
||||||
config,
|
|
||||||
tsConfig: initialTsConfig?.config,
|
|
||||||
tsConfigPath: initialTsConfig?.path,
|
|
||||||
});
|
|
||||||
await runHookConfigSetup({ settings, command: cmd, logging });
|
await runHookConfigSetup({ settings, command: cmd, logging });
|
||||||
const viteConfig = await createVite(
|
const viteConfig = await createVite(
|
||||||
{
|
{
|
||||||
|
|
|
@ -135,7 +135,7 @@ interface LoadConfigOptions {
|
||||||
validate?: boolean;
|
validate?: boolean;
|
||||||
logging: LogOptions;
|
logging: LogOptions;
|
||||||
/** Invalidate when reloading a previously loaded config */
|
/** Invalidate when reloading a previously loaded config */
|
||||||
isConfigReload?: boolean;
|
isRestart?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -222,7 +222,7 @@ async function tryLoadConfig(
|
||||||
flags: configOptions.flags,
|
flags: configOptions.flags,
|
||||||
});
|
});
|
||||||
if (!configPath) return undefined;
|
if (!configPath) return undefined;
|
||||||
if (configOptions.isConfigReload) {
|
if (configOptions.isRestart) {
|
||||||
// Hack: Write config to temporary file at project root
|
// Hack: Write config to temporary file at project root
|
||||||
// This invalidates and reloads file contents when using ESM imports or "resolve"
|
// This invalidates and reloads file contents when using ESM imports or "resolve"
|
||||||
const tempConfigPath = path.join(
|
const tempConfigPath = path.join(
|
||||||
|
|
|
@ -1,24 +1,21 @@
|
||||||
import type { TsConfigJson } from 'tsconfig-resolver';
|
|
||||||
import type { AstroConfig, AstroSettings } from '../../@types/astro';
|
import type { AstroConfig, AstroSettings } from '../../@types/astro';
|
||||||
|
|
||||||
import jsxRenderer from '../../jsx/renderer.js';
|
import jsxRenderer from '../../jsx/renderer.js';
|
||||||
|
import { loadTSConfig } from './tsconfig.js';
|
||||||
|
|
||||||
export interface CreateSettings {
|
export function createSettings(config: AstroConfig, cwd?: string): AstroSettings {
|
||||||
config: AstroConfig;
|
const tsconfig = loadTSConfig(cwd);
|
||||||
tsConfig?: TsConfigJson;
|
|
||||||
tsConfigPath?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createSettings({ config, tsConfig, tsConfigPath }: CreateSettings): AstroSettings {
|
|
||||||
return {
|
return {
|
||||||
config,
|
config,
|
||||||
tsConfig,
|
tsConfig: tsconfig?.config,
|
||||||
tsConfigPath,
|
tsConfigPath: tsconfig?.path,
|
||||||
|
|
||||||
adapter: undefined,
|
adapter: undefined,
|
||||||
injectedRoutes: [],
|
injectedRoutes: [],
|
||||||
pageExtensions: ['.astro', '.md', '.html'],
|
pageExtensions: ['.astro', '.md', '.html'],
|
||||||
renderers: [jsxRenderer],
|
renderers: [jsxRenderer],
|
||||||
scripts: [],
|
scripts: [],
|
||||||
|
watchFiles: tsconfig?.exists ? [tsconfig.path, ...tsconfig.extendedPaths] : [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,12 @@ export default async function dev(
|
||||||
const devStart = performance.now();
|
const devStart = performance.now();
|
||||||
applyPolyfill();
|
applyPolyfill();
|
||||||
await options.telemetry.record([]);
|
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 { host, port } = settings.config.server;
|
||||||
const { isRestart = false } = options;
|
const { isRestart = false } = options;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { bold } from 'kleur/colors';
|
import { bold } from 'kleur/colors';
|
||||||
import type { AddressInfo } from 'net';
|
import type { AddressInfo } from 'net';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
import type { InlineConfig, ViteDevServer } from 'vite';
|
import type { InlineConfig, ViteDevServer } from 'vite';
|
||||||
import {
|
import {
|
||||||
AstroConfig,
|
AstroConfig,
|
||||||
|
@ -37,10 +38,12 @@ export async function runHookConfigSetup({
|
||||||
settings,
|
settings,
|
||||||
command,
|
command,
|
||||||
logging,
|
logging,
|
||||||
|
isRestart = false,
|
||||||
}: {
|
}: {
|
||||||
settings: AstroSettings;
|
settings: AstroSettings;
|
||||||
command: 'dev' | 'build';
|
command: 'dev' | 'build';
|
||||||
logging: LogOptions;
|
logging: LogOptions;
|
||||||
|
isRestart?: boolean;
|
||||||
}): Promise<AstroSettings> {
|
}): Promise<AstroSettings> {
|
||||||
// An adapter is an integration, so if one is provided push it.
|
// An adapter is an integration, so if one is provided push it.
|
||||||
if (settings.config.adapter) {
|
if (settings.config.adapter) {
|
||||||
|
@ -66,6 +69,7 @@ export async function runHookConfigSetup({
|
||||||
const hooks: HookParameters<'astro:config:setup'> = {
|
const hooks: HookParameters<'astro:config:setup'> = {
|
||||||
config: updatedConfig,
|
config: updatedConfig,
|
||||||
command,
|
command,
|
||||||
|
isRestart,
|
||||||
addRenderer(renderer: AstroRenderer) {
|
addRenderer(renderer: AstroRenderer) {
|
||||||
if (!renderer.name) {
|
if (!renderer.name) {
|
||||||
throw new Error(`Integration ${bold(integration.name)} has an unnamed renderer.`);
|
throw new Error(`Integration ${bold(integration.name)} has an unnamed renderer.`);
|
||||||
|
@ -86,6 +90,9 @@ export async function runHookConfigSetup({
|
||||||
injectRoute: (injectRoute) => {
|
injectRoute: (injectRoute) => {
|
||||||
updatedSettings.injectedRoutes.push(injectRoute);
|
updatedSettings.injectedRoutes.push(injectRoute);
|
||||||
},
|
},
|
||||||
|
addWatchFile: (path) => {
|
||||||
|
updatedSettings.watchFiles.push(path instanceof URL ? fileURLToPath(path) : path);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
// Semi-private `addPageExtension` hook
|
// Semi-private `addPageExtension` hook
|
||||||
function addPageExtension(...input: (string | string[])[]) {
|
function addPageExtension(...input: (string | string[])[]) {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { polyfill } from '@astrojs/webapi';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { loadConfig } from '../dist/core/config/config.js';
|
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 dev from '../dist/core/dev/index.js';
|
||||||
import build from '../dist/core/build/index.js';
|
import build from '../dist/core/build/index.js';
|
||||||
import preview from '../dist/core/preview/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('/')) {
|
if (inlineConfig.base && !inlineConfig.base.endsWith('/')) {
|
||||||
config.base = inlineConfig.base + '/';
|
config.base = inlineConfig.base + '/';
|
||||||
}
|
}
|
||||||
let tsconfig = loadTSConfig(fileURLToPath(cwd));
|
let settings = createSettings(config, fileURLToPath(cwd));
|
||||||
let settings = createSettings({
|
|
||||||
config,
|
|
||||||
tsConfig: tsconfig?.config,
|
|
||||||
tsConfigPath: tsconfig?.path,
|
|
||||||
});
|
|
||||||
if (config.integrations.find((integration) => integration.name === '@astrojs/mdx')) {
|
if (config.integrations.find((integration) => integration.name === '@astrojs/mdx')) {
|
||||||
// Enable default JSX integration. It needs to come first, so unshift rather than push!
|
// Enable default JSX integration. It needs to come first, so unshift rather than push!
|
||||||
const { default: jsxRenderer } = await import('astro/jsx/renderer.js');
|
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 type { AstroIntegration } from 'astro';
|
||||||
import autoprefixerPlugin from 'autoprefixer';
|
import autoprefixerPlugin from 'autoprefixer';
|
||||||
|
import fs from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import tailwindPlugin, { Config as TailwindConfig } from 'tailwindcss';
|
import tailwindPlugin, { Config as TailwindConfig } from 'tailwindcss';
|
||||||
import resolveConfig from 'tailwindcss/resolveConfig.js';
|
import resolveConfig from 'tailwindcss/resolveConfig.js';
|
||||||
|
@ -17,7 +18,7 @@ function getDefaultTailwindConfig(srcUrl: URL): TailwindConfig {
|
||||||
}) as TailwindConfig;
|
}) as TailwindConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getUserConfig(root: URL, configPath?: string) {
|
async function getUserConfig(root: URL, configPath?: string, isRestart = false) {
|
||||||
const resolvedRoot = fileURLToPath(root);
|
const resolvedRoot = fileURLToPath(root);
|
||||||
let userConfigPath: string | undefined;
|
let userConfigPath: string | undefined;
|
||||||
|
|
||||||
|
@ -26,7 +27,42 @@ async function getUserConfig(root: URL, configPath?: string) {
|
||||||
userConfigPath = fileURLToPath(new URL(configPathWithLeadingSlash, root));
|
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 =
|
type TailwindOptions =
|
||||||
|
@ -55,9 +91,9 @@ export default function tailwindIntegration(options?: TailwindOptions): AstroInt
|
||||||
return {
|
return {
|
||||||
name: '@astrojs/tailwind',
|
name: '@astrojs/tailwind',
|
||||||
hooks: {
|
hooks: {
|
||||||
'astro:config:setup': async ({ config, injectScript }) => {
|
'astro:config:setup': async ({ config, injectScript, addWatchFile, isRestart }) => {
|
||||||
// Inject the Tailwind postcss plugin
|
// Inject the Tailwind postcss plugin
|
||||||
const userConfig = await getUserConfig(config.root, customConfigPath);
|
const userConfig = await getUserConfig(config.root, customConfigPath, isRestart);
|
||||||
|
|
||||||
if (customConfigPath && !userConfig?.value) {
|
if (customConfigPath && !userConfig?.value) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -67,6 +103,10 @@ export default function tailwindIntegration(options?: TailwindOptions): AstroInt
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userConfig?.filePath) {
|
||||||
|
addWatchFile(userConfig.filePath);
|
||||||
|
}
|
||||||
|
|
||||||
const tailwindConfig: TailwindConfig =
|
const tailwindConfig: TailwindConfig =
|
||||||
(userConfig?.value as TailwindConfig) ?? getDefaultTailwindConfig(config.srcDir);
|
(userConfig?.value as TailwindConfig) ?? getDefaultTailwindConfig(config.srcDir);
|
||||||
config.style.postcss.plugins.push(tailwindPlugin(tailwindConfig));
|
config.style.postcss.plugins.push(tailwindPlugin(tailwindConfig));
|
||||||
|
|
Loading…
Reference in a new issue