From a5e3ecc8039c1e115ce5597362e18cd35d04e40b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Mart=C3=ADn=20Seery?= Date: Wed, 12 Oct 2022 12:36:33 -0300 Subject: [PATCH] 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 --- .changeset/new-hotels-unite.md | 6 ++ .changeset/ten-candles-relate.md | 7 ++ packages/astro/src/@types/astro.ts | 3 + packages/astro/src/cli/index.ts | 86 ++++++++++----------- packages/astro/src/config/index.ts | 9 +-- packages/astro/src/core/config/config.ts | 4 +- packages/astro/src/core/config/settings.ts | 15 ++-- packages/astro/src/core/dev/index.ts | 7 +- packages/astro/src/integrations/index.ts | 7 ++ packages/astro/test/test-utils.js | 9 +-- packages/integrations/tailwind/src/index.ts | 50 ++++++++++-- 11 files changed, 129 insertions(+), 74 deletions(-) create mode 100644 .changeset/new-hotels-unite.md create mode 100644 .changeset/ten-candles-relate.md diff --git a/.changeset/new-hotels-unite.md b/.changeset/new-hotels-unite.md new file mode 100644 index 000000000..8febc2d47 --- /dev/null +++ b/.changeset/new-hotels-unite.md @@ -0,0 +1,6 @@ +--- +'astro': minor +--- + +- Added `isRestart` and `addWatchFile` to integration step `isRestart`. +- Restart dev server automatically when tsconfig changes. diff --git a/.changeset/ten-candles-relate.md b/.changeset/ten-candles-relate.md new file mode 100644 index 000000000..402e46a1c --- /dev/null +++ b/.changeset/ten-candles-relate.md @@ -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. diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 0d6e4d5a5..b95aae047 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -891,6 +891,7 @@ export interface AstroSettings { }[]; tsConfig: TsConfigJson | undefined; tsConfigPath: string | undefined; + watchFiles: string[]; } export type AsyncRendererComponentFn = ( @@ -1142,8 +1143,10 @@ export interface AstroIntegration { 'astro:config:setup'?: (options: { config: AstroConfig; command: 'dev' | 'build'; + isRestart: boolean; updateConfig: (newConfig: Record) => 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. diff --git a/packages/astro/src/cli/index.ts b/packages/astro/src/cli/index.ts index 93e589396..ab52f6415 100644 --- a/packages/astro/src/cli/index.ts +++ b/packages/astro/src/cli/index.ts @@ -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 }); } }; diff --git a/packages/astro/src/config/index.ts b/packages/astro/src/config/index.ts index 8b38ed354..6601a7a5a 100644 --- a/packages/astro/src/config/index.ts +++ b/packages/astro/src/config/index.ts @@ -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( { diff --git a/packages/astro/src/core/config/config.ts b/packages/astro/src/core/config/config.ts index bbaeee186..07f6b0320 100644 --- a/packages/astro/src/core/config/config.ts +++ b/packages/astro/src/core/config/config.ts @@ -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( diff --git a/packages/astro/src/core/config/settings.ts b/packages/astro/src/core/config/settings.ts index a69697726..8b7bfbec8 100644 --- a/packages/astro/src/core/config/settings.ts +++ b/packages/astro/src/core/config/settings.ts @@ -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] : [], }; } diff --git a/packages/astro/src/core/dev/index.ts b/packages/astro/src/core/dev/index.ts index 2979deeff..bd3659671 100644 --- a/packages/astro/src/core/dev/index.ts +++ b/packages/astro/src/core/dev/index.ts @@ -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; diff --git a/packages/astro/src/integrations/index.ts b/packages/astro/src/integrations/index.ts index d533a1a8e..9a9acd2dd 100644 --- a/packages/astro/src/integrations/index.ts +++ b/packages/astro/src/integrations/index.ts @@ -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 { // 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[])[]) { diff --git a/packages/astro/test/test-utils.js b/packages/astro/test/test-utils.js index c1fe6958c..a4f9191f9 100644 --- a/packages/astro/test/test-utils.js +++ b/packages/astro/test/test-utils.js @@ -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'); diff --git a/packages/integrations/tailwind/src/index.ts b/packages/integrations/tailwind/src/index.ts index 1e5008f6c..2f1b68e28 100644 --- a/packages/integrations/tailwind/src/index.ts +++ b/packages/integrations/tailwind/src/index.ts @@ -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));