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: support tailwind config files #2831

Merged
merged 13 commits into from
Mar 21, 2022
Merged
1 change: 1 addition & 0 deletions packages/integrations/tailwind/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
},
"devDependencies": {
"@types/tailwindcss": "^3.0.9",
"@proload/core": "^0.2.2",
"astro": "workspace:*",
"astro-scripts": "workspace:*"
},
Expand Down
56 changes: 52 additions & 4 deletions packages/integrations/tailwind/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import type { AstroIntegration } from 'astro';
import { fileURLToPath } from 'url';
import path from 'path';
import tailwindPlugin from 'tailwindcss';
import type { TailwindConfig } from 'tailwindcss/tailwind-config';
import autoprefixerPlugin from 'autoprefixer';
import load from '@proload/core';

function getDefaultTailwindConfig(srcUrl: URL) {
function getDefaultTailwindConfig(srcUrl: URL): TailwindConfig {
return {
theme: {
extend: {},
Expand All @@ -14,13 +16,59 @@ function getDefaultTailwindConfig(srcUrl: URL) {
};
}

export default function (): AstroIntegration {
async function getUserConfig(projectRoot: URL, configPath?: string) {
const resolvedProjectRoot = fileURLToPath(projectRoot);
let userConfigPath: string | undefined;

if (configPath) {
const configPathWithLeadingSlash = /^\.*\//.test(configPath) ? configPath : `./${configPath}`;
userConfigPath = fileURLToPath(new URL(configPathWithLeadingSlash, `file://${resolvedProjectRoot}/`));
}

return await load('tailwind', { mustExist: false, cwd: resolvedProjectRoot, filePath: userConfigPath });
}

type IntegrationConfig =
| {
config?: {
/**
* Path to your tailwind config file
* @default 'tailwind.config.js'
*/
path?: string;
/**
* Apply Astro's default Tailwind config as a preset
* This is recommended to enable Tailwind across all components and Astro files
* @default true
*/
applyAstroPreset?: boolean;
};
}
| undefined;

export default function (integrationConfig: IntegrationConfig): AstroIntegration {
const applyAstroConfigPreset = integrationConfig?.config?.applyAstroPreset ?? true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

something I'll keep in mind: setting default variables options seems to be a common use-case for integrations. Nuxt ships with a helper for integrations to define default options easily, maybe could do the same in the future.

return {
name: '@astrojs/tailwind',
hooks: {
'astro:config:setup': ({ config, injectScript }) => {
'astro:config:setup': async ({ config, injectScript }) => {
// Inject the Tailwind postcss plugin
config.styleOptions.postcss.plugins.push(tailwindPlugin(getDefaultTailwindConfig(config.src)));
const userConfig = await getUserConfig(config.projectRoot, integrationConfig?.config?.path);

let tailwindConfig: TailwindConfig;
if (typeof userConfig?.value === 'object' && userConfig.value !== null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If userConfig?.value returned something but that something wasn't an object, I almost would want this to be an error that the user sees, instead of silently failing this check and using the default config below.

Can we do the simpler if (userConfig && userConfig.value) { here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, taking a step further back: what happens if options.path is given to the integration, but then fails to load? I'd expect that to be an error for the user to investigate & fix.

Another good reason to do this is that it should really let you simplify the code below where you're trying to handle this tricky case. For example, I think it would mean that you could just do this, below:

/* throw if options.path was given, but userConfig or userConfig.value failed to load */

const tailwindConfig = userConfing.value ?? getDefaultTailwindConfig(config.src);
if (applyAstroConfigPreset) {
  tailwindConfig.presets.push(getDefaultTailwindConfig(config.src));
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, great thinking! One issue with this refactor though: it'll throw an error when a tailwind.config is missing no matter what. Ideally, it would log a human-readable error specifically when you pass a bad config.path. Made a refactor here.

if (applyAstroConfigPreset) {
tailwindConfig = {
presets: [getDefaultTailwindConfig(config.src), ...(userConfig.value.presets || [])],
...(userConfig.value as TailwindConfig),
};
} else {
tailwindConfig = userConfig.value as TailwindConfig;
}
} else {
tailwindConfig = getDefaultTailwindConfig(config.src);
}
config.styleOptions.postcss.plugins.push(tailwindPlugin(tailwindConfig));
config.styleOptions.postcss.plugins.push(autoprefixerPlugin);

// Inject the Tailwind base import
Expand Down
3 changes: 2 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.