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 `isConfigReload` and `injectWatchTarget` to integration step `isConfigReload`.
- Restart dev server automatically when tsconfig changes.
5 changes: 5 additions & 0 deletions .changeset/ten-candles-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/tailwind': minor
---

Restart dev server automatically when tailwind config changes.
7 changes: 7 additions & 0 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,10 @@ export interface InjectedRoute {
pattern: string;
entryPoint: string;
}
export interface InjectedWatchTarget {
path: string;
type: 'relative' | 'absolute';
}
export interface AstroConfig extends z.output<typeof AstroConfigSchema> {
// Public:
// This is a more detailed type than zod validation gives us.
Expand All @@ -891,6 +895,7 @@ export interface AstroSettings {
}[];
tsConfig: TsConfigJson | undefined;
tsConfigPath: string | undefined;
watchTargets: InjectedWatchTarget[];
}

export type AsyncRendererComponentFn<U> = (
Expand Down Expand Up @@ -1142,10 +1147,12 @@ export interface AstroIntegration {
'astro:config:setup'?: (options: {
config: AstroConfig;
command: 'dev' | 'build';
isConfigReload: boolean;
JuanM04 marked this conversation as resolved.
Show resolved Hide resolved
updateConfig: (newConfig: Record<string, any>) => void;
addRenderer: (renderer: AstroRenderer) => void;
injectScript: (stage: InjectedScriptStage, content: string) => void;
injectRoute: (injectRoute: InjectedRoute) => void;
injectWatchTarget: (target: InjectedWatchTarget) => void;
JuanM04 marked this conversation as resolved.
Show resolved Hide resolved
// TODO: Add support for `injectElement()` for full HTML element injection, not just scripts.
// This may require some refactoring of `scripts`, `styles`, and `links` into something
// more generalized. Consider the SSR use-case as well.
Expand Down
87 changes: 51 additions & 36 deletions packages/astro/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,42 +191,57 @@ 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.watchTargets.length > 0) {
// If the config file didn't change, check if any of the watched files changed.
shouldRestart = settings.watchTargets.some(({ path, type }) => {
const target =
type === 'absolute'
? normalizePath(path)
: `${normalizePath(resolvedRoot)}/${normalizePath(path)}`;
return target === normalizePath(changedFile);
});
}

if (!shouldRestart) return;

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 });
}
};

Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/config/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ export function createSettings({ config, tsConfig, tsConfigPath }: CreateSetting
pageExtensions: ['.astro', '.md', '.html'],
renderers: [jsxRenderer],
scripts: [],
watchTargets: tsConfigPath ? [{ path: tsConfigPath, type: 'absolute' }] : [],
JuanM04 marked this conversation as resolved.
Show resolved Hide resolved
};
}
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,
isConfigReload: options.isRestart,
});
const { host, port } = settings.config.server;
const { isRestart = false } = options;

Expand Down
6 changes: 6 additions & 0 deletions packages/astro/src/integrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,12 @@ export async function runHookConfigSetup({
settings,
command,
logging,
isConfigReload = false,
}: {
settings: AstroSettings;
command: 'dev' | 'build';
logging: LogOptions;
isConfigReload?: boolean;
}): Promise<AstroSettings> {
// An adapter is an integration, so if one is provided push it.
if (settings.config.adapter) {
Expand All @@ -67,6 +69,7 @@ export async function runHookConfigSetup({
const hooks: HookParameters<'astro:config:setup'> = {
config: updatedConfig,
command,
isConfigReload,
addRenderer(renderer: AstroRenderer) {
if (!renderer.name) {
throw new Error(`Integration ${bold(integration.name)} has an unnamed renderer.`);
Expand All @@ -87,6 +90,9 @@ export async function runHookConfigSetup({
injectRoute: (injectRoute) => {
updatedSettings.injectedRoutes.push(injectRoute);
},
injectWatchTarget: (target) => {
updatedSettings.watchTargets.push(target);
},
};
// Semi-private `addPageExtension` hook
function addPageExtension(...input: (string | string[])[]) {
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, isConfigReload: boolean = 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 (isConfigReload) {
// 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, injectWatchTarget, isConfigReload }) => {
// Inject the Tailwind postcss plugin
const userConfig = await getUserConfig(config.root, customConfigPath);
const userConfig = await getUserConfig(config.root, customConfigPath, isConfigReload);

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

if (userConfig?.filePath) {
injectWatchTarget({ path: userConfig.filePath, type: 'absolute' });
}

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