Skip to content

Commit

Permalink
feat: restart dev server when tsconfig and tailwind config changes (#…
Browse files Browse the repository at this point in the history
…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>
  • Loading branch information
JuanM04 and matthewp committed Oct 12, 2022
1 parent 5412c0c commit a5e3ecc
Show file tree
Hide file tree
Showing 11 changed files with 129 additions and 74 deletions.
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

0 comments on commit a5e3ecc

Please sign in to comment.