From 24aec54c6bb095894b8764f8764ab7c864414fac Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Fri, 20 Jan 2023 15:52:21 +1100 Subject: [PATCH 01/17] add built-in PostCSS and Tailwind support --- .changeset/gorgeous-meals-rescue.md | 8 + .changeset/lemon-humans-yell.md | 8 + docs/guides/styling.md | 121 +++++- integration/css-modules-test.ts | 2 + integration/css-side-effect-imports-test.ts | 2 + .../deterministic-build-output-test.ts | 1 + integration/postcss-test.ts | 347 ++++++++++++++++++ integration/tailwind-test.ts | 218 +++++++++++ integration/vanilla-extract-test.ts | 2 + package.json | 1 + .../remix-dev/__tests__/readConfig-test.ts | 4 + packages/remix-dev/compiler/compileBrowser.ts | 11 +- packages/remix-dev/compiler/compilerServer.ts | 11 +- .../compiler/plugins/cssFilePlugin.ts | 56 ++- .../compiler/plugins/cssModulesPlugin.ts | 19 +- .../plugins/cssSideEffectImportsPlugin.ts | 25 +- .../compiler/plugins/vanillaExtractPlugin.ts | 16 +- packages/remix-dev/compiler/utils/postcss.ts | 129 +++++++ packages/remix-dev/config.ts | 4 + packages/remix-dev/package.json | 1 + packages/remix-react/entry.ts | 2 + packages/remix-server-runtime/entry.ts | 2 + packages/remix-testing/create-remix-stub.tsx | 2 + yarn.lock | 174 ++++++++- 24 files changed, 1118 insertions(+), 48 deletions(-) create mode 100644 .changeset/gorgeous-meals-rescue.md create mode 100644 .changeset/lemon-humans-yell.md create mode 100644 integration/postcss-test.ts create mode 100644 integration/tailwind-test.ts create mode 100644 packages/remix-dev/compiler/utils/postcss.ts diff --git a/.changeset/gorgeous-meals-rescue.md b/.changeset/gorgeous-meals-rescue.md new file mode 100644 index 00000000000..bbebd5a1d83 --- /dev/null +++ b/.changeset/gorgeous-meals-rescue.md @@ -0,0 +1,8 @@ +--- +"@remix-run/dev": minor +"@remix-run/react": minor +"@remix-run/server-runtime": minor +"@remix-run/testing": minor +--- + +Add unstable built-in support for PostCSS via the `future.unstable_postcss` feature flag diff --git a/.changeset/lemon-humans-yell.md b/.changeset/lemon-humans-yell.md new file mode 100644 index 00000000000..902b69c7603 --- /dev/null +++ b/.changeset/lemon-humans-yell.md @@ -0,0 +1,8 @@ +--- +"@remix-run/dev": minor +"@remix-run/react": minor +"@remix-run/server-runtime": minor +"@remix-run/testing": minor +--- + +Add unstable built-in support for Tailwind via the `future.unstable_tailwind` feature flag diff --git a/docs/guides/styling.md b/docs/guides/styling.md index 611ec55bb89..cdaef2824ef 100644 --- a/docs/guides/styling.md +++ b/docs/guides/styling.md @@ -400,7 +400,80 @@ export function links() { ## Tailwind CSS -Perhaps the most popular way to style a Remix application in the community is to use Tailwind CSS. It has the benefits of inline-style collocation for developer ergonomics and is able to generate a CSS file for Remix to import. The generated CSS file generally caps out around 8-10kb, even for large applications. Load that file into the `root.tsx` links and be done with it. If you don't have any CSS opinions, this is a great approach. +Perhaps the most popular way to style a Remix application in the community is to use [Tailwind CSS][tailwind]. It has the benefits of inline-style collocation for developer ergonomics and is able to generate a CSS file for Remix to import. The generated CSS file generally caps out around 8-10kb, even for large applications. Load that file into the `root.tsx` links and be done with it. If you don't have any CSS opinions, this is a great approach. + +There are a couple of options for integrating Tailwind into Remix. You can leverage the new, experimental built-in support, or integrate Tailwind manually using their CLI. + +### Built-in Tailwind Support + +This feature is unstable and currently only available behind a feature flag. We're confident in the use cases it solves but the API and implementation may change in the future. + +First, to enable built-in Tailwind support, set the `future.unstable_tailwind` feature flag in `remix.config.js`. + +```js filename=remix.config.js +/** @type {import('@remix-run/dev').AppConfig} */ +module.exports = { + future: { + unstable_tailwind: true, + }, + // ... +}; +``` + +Then install Tailwind: + +```sh +npm install -D tailwind +``` + +Initialize a config file: + +```sh +npx tailwindcss init +``` + +Now we can tell it which files to generate classes from: + +```js filename=tailwind.config.js lines=[3] +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["./app/**/*.{ts,tsx,jsx,js}"], + theme: { + extend: {}, + }, + plugins: [], +}; +``` + +Then include the `@tailwind` directives in your CSS. For example, you could create a `tailwind.css` file at the root of your app: + +```css filename=app/tailwind.css +@tailwind base; +@tailwind components; +@tailwind utilities; +``` + +Then add `tailwind.css` to your root route's `links` function: + +```tsx filename=app/root.tsx +import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno + +// ... + +import styles from "./tailwind.css"; + +export const links: LinksFunction = () => [ + { rel: "stylesheet", href: styles }, +]; +``` + +With this setup in place, you can also use any of [Tailwind's functions and directives][tailwind-functions-and-directives] anywhere in your CSS. + +Note that if you're also using Remix's [built-in PostCSS support](#built-in-postcss-support), the Tailwind PostCSS plugin will be automatically included if it's missing. + +### Manual Tailwind Integration + +It's also possible to use Tailwind without leveraging the built-in support by using the `tailwindcss` CLI directly. First install a couple dev dependencies: @@ -527,7 +600,47 @@ export const links: LinksFunction = () => { ## PostCSS -While not built into Remix's compiler, it is straight forward to use PostCSS and add whatever syntax sugar you'd like to your stylesheets, here's the gist of it: +[PostCSS][postcss] is a popular tool with a rich plugin ecosystem, commonly used to prefix CSS for older browsers, transpile future CSS syntax, inline images, lint your styles and more. + +There are a couple of options for integrating PostCSS into Remix. You can leverage the new, experimental built-in support, or integrate PostCSS manually using their CLI. + +### Built-in PostCSS Support + +This feature is unstable and currently only available behind a feature flag. We're confident in the use cases it solves but the API and implementation may change in the future. + +When a PostCSS config is detected, PostCSS will automatically be run across all CSS in your project. For example, to use [Autoprefixer][autoprefixer]: + +1. Enable built-in PostCSS support by setting the the `future.unstable_postcss` feature flag in `remix.config.js`. + + ```js filename=remix.config.js + /** @type {import('@remix-run/dev').AppConfig} */ + module.exports = { + future: { + unstable_postcss: true, + }, + // ... + }; + ``` + +2. Install the dependency: + + ```sh + npm install -D autoprefixer + ``` + +3. Add `postcss.config.js` in the Remix root referencing the plugin: + + ```js filename=postcss.config.js + module.exports = { + plugins: { + autoprefixer: {}, + }, + }; + ``` + +### Manual PostCSS Integration + +It's also possible to use PostCSS without leveraging the built-in support. Here's the gist of it: 1. Use `postcss` cli directly alongside Remix 2. Build CSS into the Remix app directory from a styles source directory @@ -942,7 +1055,11 @@ module.exports = { [examples]: https://github.com/remix-run/examples [styled-components-issue]: https://github.com/styled-components/styled-components/issues/3660 [tailwind]: https://tailwindcss.com +[tailwind-functions-and-directives]: https://tailwindcss.com/docs/functions-and-directives [tailwind-intelli-sense-extension]: https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss +[postcss]: https://postcss.org +[autoprefixer]: https://github.com/postcss/autoprefixer +[postcss-preset-env]: https://preset-env.cssdb.org [css modules]: https://github.com/css-modules/css-modules [regular-stylesheet-imports]: #regular-stylesheets [server-dependencies-to-bundle]: ../file-conventions/remix-config#serverdependenciestobundle diff --git a/integration/css-modules-test.ts b/integration/css-modules-test.ts index eea0747eb91..bc617512bb0 100644 --- a/integration/css-modules-test.ts +++ b/integration/css-modules-test.ts @@ -27,6 +27,8 @@ test.describe("CSS Modules", () => { // ensure features don't clash unstable_cssModules: true, unstable_cssSideEffectImports: true, + unstable_postcss: true, + unstable_tailwind: true, unstable_vanillaExtract: true, }, }; diff --git a/integration/css-side-effect-imports-test.ts b/integration/css-side-effect-imports-test.ts index b1f687596c2..3a72c4d10d5 100644 --- a/integration/css-side-effect-imports-test.ts +++ b/integration/css-side-effect-imports-test.ts @@ -26,6 +26,8 @@ test.describe("CSS side-effect imports", () => { // ensure features don't clash unstable_cssModules: true, unstable_cssSideEffectImports: true, + unstable_postcss: true, + unstable_tailwind: true, unstable_vanillaExtract: true, }, }; diff --git a/integration/deterministic-build-output-test.ts b/integration/deterministic-build-output-test.ts index 3089996411b..05b35902158 100644 --- a/integration/deterministic-build-output-test.ts +++ b/integration/deterministic-build-output-test.ts @@ -31,6 +31,7 @@ test("builds deterministically under different paths", async () => { future: { unstable_cssModules: true, unstable_cssSideEffectImports: true, + unstable_postcss: true, unstable_vanillaExtract: true, }, }; diff --git a/integration/postcss-test.ts b/integration/postcss-test.ts new file mode 100644 index 00000000000..31c149ce03b --- /dev/null +++ b/integration/postcss-test.ts @@ -0,0 +1,347 @@ +import { test, expect } from "@playwright/test"; +import type { Page } from "@playwright/test"; + +import { PlaywrightFixture } from "./helpers/playwright-fixture"; +import type { Fixture, AppFixture } from "./helpers/create-fixture"; +import { + createAppFixture, + createFixture, + css, + js, +} from "./helpers/create-fixture"; + +const TEST_PADDING_VALUE = "20px"; + +async function jsonFromBase64CssContent({ + page, + testId, +}: { + page: Page; + testId: string; +}) { + let locator = await page.locator(`[data-testid=${testId}]`); + let content = await locator.evaluate( + (el) => getComputedStyle(el, ":after").content + ); + let json = Buffer.from(content.replace(/"/g, ""), "base64").toString("utf-8"); + return JSON.parse(json); +} + +test.describe("PostCSS", () => { + let fixture: Fixture; + let appFixture: AppFixture; + + test.beforeAll(async () => { + fixture = await createFixture({ + files: { + "remix.config.js": js` + module.exports = { + future: { + // Enable all CSS future flags to + // ensure features don't clash + unstable_cssModules: true, + unstable_cssSideEffectImports: true, + unstable_postcss: true, + unstable_tailwind: true, + unstable_vanillaExtract: true, + }, + }; + `, + "postcss.config.js": js` + module.exports = (ctx) => ({ + plugins: [ + { + postcssPlugin: 'replace', + Declaration (decl) { + decl.value = decl.value + .replaceAll( + "TEST_PADDING_VALUE", + ${JSON.stringify(TEST_PADDING_VALUE)}, + ) + .replaceAll( + "TEST_POSTCSS_CONTEXT", + Buffer.from(JSON.stringify(ctx)).toString("base64"), + ); + }, + }, + ], + }); + `, + "tailwind.config.js": js` + module.exports = { + content: ["./app/**/*.{ts,tsx,jsx,js}"], + theme: { + spacing: { + 'test': ${JSON.stringify(TEST_PADDING_VALUE)} + }, + }, + }; + `, + "app/root.jsx": js` + import { Links, Outlet } from "@remix-run/react"; + import { cssBundleHref } from "@remix-run/css-bundle"; + export function links() { + return [ + { rel: "stylesheet", href: cssBundleHref } + ]; + } + export default function Root() { + return ( + + + + + + + + + ) + } + `, + ...regularStylesSheetsFixture(), + ...cssModulesFixture(), + ...vanillaExtractFixture(), + ...cssSideEffectImportsFixture(), + ...automaticTailwindPluginInsertionFixture(), + }, + }); + appFixture = await createAppFixture(fixture); + }); + + test.afterAll(async () => { + await appFixture.close(); + }); + + let regularStylesSheetsFixture = () => ({ + "app/routes/regular-style-sheets-test.jsx": js` + import { Test, links as testLinks } from "~/test-components/regular-style-sheets"; + + export function links() { + return [...testLinks()]; + } + + export default function() { + return ; + } + `, + "app/test-components/regular-style-sheets/index.jsx": js` + import stylesHref from "./styles.css"; + + export function links() { + return [{ rel: 'stylesheet', href: stylesHref }]; + } + + export function Test() { + return ( +
+

Regular style sheets test.

+

PostCSS context (base64):

+
+ ); + } + `, + "app/test-components/regular-style-sheets/styles.css": css` + .regular-style-sheets-test { + padding: TEST_PADDING_VALUE; + } + + [data-testid="regular-style-sheets-postcss-context"]:after { + content: "TEST_POSTCSS_CONTEXT"; + } + `, + }); + test("regular style sheets", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/regular-style-sheets-test"); + let locator = await page.locator("[data-testid='regular-style-sheets']"); + let padding = await locator.evaluate((el) => getComputedStyle(el).padding); + expect(padding).toBe(TEST_PADDING_VALUE); + }); + test("regular style sheets PostCSS context", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/regular-style-sheets-test"); + let testId = "regular-style-sheets-postcss-context"; + let postcssContext = await jsonFromBase64CssContent({ page, testId }); + expect(postcssContext.remix.vanillaExtract).toBe(false); + }); + + let cssModulesFixture = () => ({ + "app/routes/css-modules-test.jsx": js` + import { Test } from "~/test-components/css-modules"; + + export default function() { + return ; + } + `, + "app/test-components/css-modules/index.jsx": js` + import styles from "./styles.module.css"; + + export function Test() { + return ( +
+

CSS Modules test.

+

PostCSS context (base64):

+
+ ); + } + `, + "app/test-components/css-modules/styles.module.css": css` + .root { + padding: TEST_PADDING_VALUE; + } + + [data-testid="css-modules-postcss-context"]:after { + content: "TEST_POSTCSS_CONTEXT"; + } + `, + }); + test("CSS Modules", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/css-modules-test"); + let locator = await page.locator("[data-testid='css-modules']"); + let padding = await locator.evaluate((el) => getComputedStyle(el).padding); + expect(padding).toBe(TEST_PADDING_VALUE); + }); + test("CSS Modules PostCSS context", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/css-modules-test"); + let testId = "css-modules-postcss-context"; + let postcssContext = await jsonFromBase64CssContent({ page, testId }); + expect(postcssContext.remix.vanillaExtract).toBe(false); + }); + + let vanillaExtractFixture = () => ({ + "app/routes/vanilla-extract-test.jsx": js` + import { Test } from "~/test-components/vanilla-extract"; + + export default function() { + return ; + } + `, + "app/test-components/vanilla-extract/index.jsx": js` + import * as styles from "./styles.css"; + + export function Test() { + return ( +
+

Vanilla Extract test.

+

PostCSS context (base64):

+
+ ); + } + `, + "app/test-components/vanilla-extract/styles.css.ts": css` + import { style, globalStyle } from "@vanilla-extract/css"; + + export const root = style({ + padding: "TEST_PADDING_VALUE", + }); + + globalStyle('[data-testid="vanilla-extract-postcss-context"]:after', { + content: "TEST_POSTCSS_CONTEXT", + }) + `, + }); + test("Vanilla Extract", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/vanilla-extract-test"); + let locator = await page.locator("[data-testid='vanilla-extract']"); + let padding = await locator.evaluate((el) => getComputedStyle(el).padding); + expect(padding).toBe(TEST_PADDING_VALUE); + }); + test("Vanilla Extract PostCSS context", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/vanilla-extract-test"); + let testId = "vanilla-extract-postcss-context"; + let postcssContext = await jsonFromBase64CssContent({ page, testId }); + expect(postcssContext.remix.vanillaExtract).toBe(true); + }); + + let cssSideEffectImportsFixture = () => ({ + "app/routes/css-side-effect-imports-test.jsx": js` + import { Test } from "~/test-components/css-side-effect-imports"; + + export default function() { + return ; + } + `, + "app/test-components/css-side-effect-imports/index.jsx": js` + import "./styles.css"; + + export function Test() { + return ( +
+

CSS side-effect imports test.

+

PostCSS context (base64):

+
+ ); + } + `, + "app/test-components/css-side-effect-imports/styles.css": css` + .css-side-effect-imports-test { + padding: TEST_PADDING_VALUE; + } + + [data-testid="css-side-effect-imports-postcss-context"]:after { + content: "TEST_POSTCSS_CONTEXT"; + } + `, + }); + test("CSS side-effect imports", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/css-side-effect-imports-test"); + let locator = await page.locator("[data-testid='css-side-effect-imports']"); + let padding = await locator.evaluate((el) => getComputedStyle(el).padding); + expect(padding).toBe(TEST_PADDING_VALUE); + }); + test("CSS side-effect imports PostCSS context", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/css-side-effect-imports-test"); + let testId = "css-side-effect-imports-postcss-context"; + let postcssContext = await jsonFromBase64CssContent({ page, testId }); + expect(postcssContext.remix.vanillaExtract).toBe(false); + }); + + let automaticTailwindPluginInsertionFixture = () => ({ + "app/routes/automatic-tailwind-plugin-insertion-test.jsx": js` + import { Test, links as testLinks } from "~/test-components/automatic-tailwind-plugin-insertion"; + + export function links() { + return [...testLinks()]; + } + + export default function() { + return ; + } + `, + "app/test-components/automatic-tailwind-plugin-insertion/index.jsx": js` + import stylesHref from "./styles.css"; + + export function links() { + return [{ rel: 'stylesheet', href: stylesHref }]; + } + + export function Test() { + return ( +
+ Automatic Tailwind plugin test +
+ ); + } + `, + "app/test-components/automatic-tailwind-plugin-insertion/styles.css": css` + .automatic-tailwind-plugin-insertion-test { + @apply p-test; + } + `, + }); + test("automatic Tailwind plugin insertion", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/automatic-tailwind-plugin-insertion-test"); + let locator = await page.locator( + "[data-testid='automatic-tailwind-plugin-insertion']" + ); + let padding = await locator.evaluate((el) => getComputedStyle(el).padding); + expect(padding).toBe(TEST_PADDING_VALUE); + }); +}); diff --git a/integration/tailwind-test.ts b/integration/tailwind-test.ts new file mode 100644 index 00000000000..a3cf4084f19 --- /dev/null +++ b/integration/tailwind-test.ts @@ -0,0 +1,218 @@ +import { test, expect } from "@playwright/test"; + +import { PlaywrightFixture } from "./helpers/playwright-fixture"; +import type { Fixture, AppFixture } from "./helpers/create-fixture"; +import { + createAppFixture, + createFixture, + css, + js, +} from "./helpers/create-fixture"; + +const TEST_PADDING_VALUE = "20px"; + +test.describe("Tailwind", () => { + let fixture: Fixture; + let appFixture: AppFixture; + + test.beforeAll(async () => { + fixture = await createFixture({ + files: { + "remix.config.js": js` + module.exports = { + future: { + // Enable all CSS future flags to + // ensure features don't clash + unstable_cssModules: true, + unstable_cssSideEffectImports: true, + unstable_postcss: true, + unstable_tailwind: true, + unstable_vanillaExtract: true, + }, + }; + `, + "tailwind.config.js": js` + module.exports = { + content: ["./app/**/*.{ts,tsx,jsx,js}"], + theme: { + spacing: { + 'test': ${JSON.stringify(TEST_PADDING_VALUE)} + }, + }, + }; + `, + "app/tailwind.css": css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + "app/root.jsx": js` + import { Links, Outlet } from "@remix-run/react"; + import { cssBundleHref } from "@remix-run/css-bundle"; + import tailwindHref from "./tailwind.css" + export function links() { + return [ + { rel: "stylesheet", href: tailwindHref }, + { rel: "stylesheet", href: cssBundleHref } + ]; + } + export default function Root() { + return ( + + + + + + + + + ) + } + `, + ...basicUsageFixture(), + ...regularStylesSheetsFixture(), + ...cssSideEffectsFixture(), + ...cssModulesFixture(), + }, + }); + appFixture = await createAppFixture(fixture); + }); + + test.afterAll(async () => { + await appFixture.close(); + }); + + let basicUsageFixture = () => ({ + "app/routes/basic-usage-test.jsx": js` + export default function() { + return ( +
+ Basic usage test +
+ ); + } + `, + }); + test("basic usage", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/basic-usage-test"); + let locator = await page.locator("[data-testid='basic-usage']"); + let padding = await locator.evaluate( + (element) => window.getComputedStyle(element).padding + ); + expect(padding).toBe(TEST_PADDING_VALUE); + }); + + let regularStylesSheetsFixture = () => ({ + "app/routes/regular-style-sheets-test.jsx": js` + import { Test, links as testLinks } from "~/test-components/regular-style-sheets"; + + export function links() { + return [...testLinks()]; + } + + export default function() { + return ; + } + `, + "app/test-components/regular-style-sheets/index.jsx": js` + import stylesHref from "./styles.css"; + + export function links() { + return [{ rel: 'stylesheet', href: stylesHref }]; + } + + export function Test() { + return ( +
+ Regular style sheets test +
+ ); + } + `, + "app/test-components/regular-style-sheets/styles.css": css` + .regular-style-sheets-test { + @apply p-test; + } + `, + }); + test("regular style sheets", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/regular-style-sheets-test"); + let locator = await page.locator("[data-testid='regular-style-sheets']"); + let padding = await locator.evaluate( + (element) => window.getComputedStyle(element).padding + ); + expect(padding).toBe(TEST_PADDING_VALUE); + }); + + let cssModulesFixture = () => ({ + "app/routes/css-modules-test.jsx": js` + import { Test } from "~/test-components/css-modules"; + + export default function() { + return ; + } + `, + "app/test-components/css-modules/index.jsx": js` + import styles from "./styles.module.css"; + + export function Test() { + return ( +
+ CSS modules test +
+ ); + } + `, + "app/test-components/css-modules/styles.module.css": css` + .root { + @apply p-test; + } + `, + }); + test("CSS Modules", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/css-modules-test"); + let locator = await page.locator("[data-testid='css-modules']"); + let padding = await locator.evaluate( + (element) => window.getComputedStyle(element).padding + ); + expect(padding).toBe(TEST_PADDING_VALUE); + }); + + let cssSideEffectsFixture = () => ({ + "app/routes/css-side-effect-imports-test.jsx": js` + import { Test } from "~/test-components/css-side-effect-imports"; + + export default function() { + return ; + } + `, + "app/test-components/css-side-effect-imports/index.jsx": js` + import "./styles.css"; + + export function Test() { + return ( +
+ CSS side-effect imports test +
+ ); + } + `, + "app/test-components/css-side-effect-imports/styles.css": css` + .css-side-effect-imports-test { + @apply p-test; + } + `, + }); + test("CSS side-effect imports", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/css-side-effect-imports-test"); + let locator = await page.locator("[data-testid='css-side-effect-imports']"); + let padding = await locator.evaluate( + (element) => window.getComputedStyle(element).padding + ); + expect(padding).toBe(TEST_PADDING_VALUE); + }); +}); diff --git a/integration/vanilla-extract-test.ts b/integration/vanilla-extract-test.ts index 7cc199eb20f..5ce6a01189a 100644 --- a/integration/vanilla-extract-test.ts +++ b/integration/vanilla-extract-test.ts @@ -20,6 +20,8 @@ test.describe("Vanilla Extract", () => { // ensure features don't clash unstable_cssModules: true, unstable_cssSideEffectImports: true, + unstable_postcss: true, + unstable_tailwind: true, unstable_vanillaExtract: true, }, }; diff --git a/package.json b/package.json index b8d6c4ac797..2b3e25c95b6 100644 --- a/package.json +++ b/package.json @@ -125,6 +125,7 @@ "sort-package-json": "^1.55.0", "strip-indent": "^3.0.0", "to-vfile": "7.2.3", + "tailwindcss": "^3.1.8", "type-fest": "^2.16.0", "typescript": "^4.7.4", "unified": "^10.1.2", diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index a88bb37f599..929a89f8849 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -25,6 +25,8 @@ describe("readConfig", () => { future: { unstable_cssModules: expect.any(Boolean), unstable_cssSideEffectImports: expect.any(Boolean), + unstable_postcss: expect.any(Boolean), + unstable_tailwind: expect.any(Boolean), unstable_vanillaExtract: expect.any(Boolean), v2_meta: expect.any(Boolean), v2_routeConvention: expect.any(Boolean), @@ -42,6 +44,8 @@ describe("readConfig", () => { "future": Object { "unstable_cssModules": Any, "unstable_cssSideEffectImports": Any, + "unstable_postcss": Any, + "unstable_tailwind": Any, "unstable_vanillaExtract": Any, "v2_meta": Any, "v2_routeConvention": Any, diff --git a/packages/remix-dev/compiler/compileBrowser.ts b/packages/remix-dev/compiler/compileBrowser.ts index 62c8bf8da89..d1957f25d56 100644 --- a/packages/remix-dev/compiler/compileBrowser.ts +++ b/packages/remix-dev/compiler/compileBrowser.ts @@ -98,8 +98,7 @@ const createEsbuildConfig = ( } } - let { mode } = options; - let { rootDirectory } = config; + let { mode, sourcemap } = options; let outputCss = isCssBuild; let plugins: esbuild.Plugin[] = [ @@ -108,15 +107,15 @@ const createEsbuildConfig = ( ? cssBundleEntryModulePlugin(config) : null, config.future.unstable_cssModules - ? cssModulesPlugin({ mode, rootDirectory, outputCss }) + ? cssModulesPlugin({ config, mode, outputCss }) : null, config.future.unstable_vanillaExtract ? vanillaExtractPlugin({ config, mode, outputCss }) : null, config.future.unstable_cssSideEffectImports - ? cssSideEffectImportsPlugin({ rootDirectory }) + ? cssSideEffectImportsPlugin({ config }) : null, - cssFilePlugin({ mode, rootDirectory }), + cssFilePlugin({ config, mode, sourcemap }), urlImportsPlugin(), mdxPlugin(config), browserRouteModulesPlugin(config, /\?browser$/), @@ -144,7 +143,7 @@ const createEsbuildConfig = ( bundle: true, logLevel: "silent", splitting: !isCssBuild, - sourcemap: options.sourcemap, + sourcemap, // As pointed out by https://github.com/evanw/esbuild/issues/2440, when tsconfig is set to // `undefined`, esbuild will keep looking for a tsconfig.json recursively up. This unwanted // behavior can only be avoided by creating an empty tsconfig file in the root directory. diff --git a/packages/remix-dev/compiler/compilerServer.ts b/packages/remix-dev/compiler/compilerServer.ts index 9026f82495b..bdf4935c64d 100644 --- a/packages/remix-dev/compiler/compilerServer.ts +++ b/packages/remix-dev/compiler/compilerServer.ts @@ -50,22 +50,21 @@ const createEsbuildConfig = ( ); let isDenoRuntime = config.serverBuildTarget === "deno"; - let { mode } = options; - let { rootDirectory } = config; + let { mode, sourcemap } = options; let outputCss = false; let plugins: esbuild.Plugin[] = [ deprecatedRemixPackagePlugin(options.onWarning), config.future.unstable_cssModules - ? cssModulesPlugin({ mode, rootDirectory, outputCss }) + ? cssModulesPlugin({ config, mode, outputCss }) : null, config.future.unstable_vanillaExtract ? vanillaExtractPlugin({ config, mode, outputCss }) : null, config.future.unstable_cssSideEffectImports - ? cssSideEffectImportsPlugin({ rootDirectory }) + ? cssSideEffectImportsPlugin({ config }) : null, - cssFilePlugin({ mode, rootDirectory }), + cssFilePlugin({ config, mode, sourcemap }), urlImportsPlugin(), mdxPlugin(config), emptyModulesPlugin(config, /\.client(\.[jt]sx?)?$/), @@ -114,7 +113,7 @@ const createEsbuildConfig = ( // `undefined`, esbuild will keep looking for a tsconfig.json recursively up. This unwanted // behavior can only be avoided by creating an empty tsconfig file in the root directory. tsconfig: config.tsconfigPath, - sourcemap: options.sourcemap, // use linked (true) to fix up .map file + sourcemap, // use linked (true) to fix up .map file // The server build needs to know how to generate asset URLs for imports // of CSS and other files. assetNames: "_assets/[name]-[hash]", diff --git a/packages/remix-dev/compiler/plugins/cssFilePlugin.ts b/packages/remix-dev/compiler/plugins/cssFilePlugin.ts index 9f1378a89b5..863f7f1b9bc 100644 --- a/packages/remix-dev/compiler/plugins/cssFilePlugin.ts +++ b/packages/remix-dev/compiler/plugins/cssFilePlugin.ts @@ -1,9 +1,12 @@ import * as path from "path"; import * as fse from "fs-extra"; import esbuild from "esbuild"; +import type { Processor } from "postcss"; import invariant from "../../invariant"; +import type { RemixConfig } from "../../config"; import type { CompileOptions } from "../options"; +import { getPostcssProcessor } from "../utils/postcss"; const isExtendedLengthPath = /^\\\\\?\\/; @@ -15,9 +18,14 @@ function normalizePathSlashes(p: string) { * This plugin loads css files with the "css" loader (bundles and moves assets to assets directory) * and exports the url of the css file as its default export. */ -export function cssFilePlugin(options: { +export function cssFilePlugin({ + config, + mode, + sourcemap, +}: { + config: RemixConfig; mode: CompileOptions["mode"]; - rootDirectory: string; + sourcemap: CompileOptions["sourcemap"]; }): esbuild.Plugin { return { name: "css-file", @@ -25,21 +33,23 @@ export function cssFilePlugin(options: { async setup(build) { let buildOps = build.initialOptions; + let postcssProcessor = await getPostcssProcessor({ config }); + build.onLoad({ filter: /\.css$/ }, async (args) => { - let { outfile, outdir, assetNames } = buildOps; let { metafile, outputFiles, warnings, errors } = await esbuild.build({ ...buildOps, - minify: options.mode === "production", + absWorkingDir: config.rootDirectory, + minify: mode === "production", minifySyntax: true, metafile: true, write: false, - sourcemap: false, + sourcemap, incremental: false, splitting: false, stdin: undefined, outfile: undefined, - outdir: outfile ? path.dirname(outfile) : outdir, - entryNames: assetNames, + outdir: config.assetsBuildDirectory, + entryNames: buildOps.assetNames, entryPoints: [args.path], loader: { ...buildOps.loader, @@ -61,6 +71,7 @@ export function cssFilePlugin(options: { }); }, }, + ...(postcssProcessor ? [postcssPlugin(postcssProcessor)] : []), ], }); @@ -74,13 +85,13 @@ export function cssFilePlugin(options: { invariant(entry, "entry point not found"); let normalizedEntry = path.resolve( - options.rootDirectory, + config.rootDirectory, normalizePathSlashes(entry) ); let entryFile = outputFiles.find((file) => { return ( path.resolve( - options.rootDirectory, + config.rootDirectory, normalizePathSlashes(file.path) ) === normalizedEntry ); @@ -119,3 +130,30 @@ export function cssFilePlugin(options: { }, }; } + +function postcssPlugin(postcssProcessor: Processor): esbuild.Plugin { + return { + name: "postcss-plugin", + async setup(build) { + build.onLoad({ filter: /\.css$/, namespace: "file" }, async (args) => { + let contents = await fse.readFile(args.path, "utf-8"); + + contents = ( + await postcssProcessor.process(contents, { + from: args.path, + to: args.path, + map: { + inline: true, + sourcesContent: true, + }, + }) + ).css; + + return { + contents, + loader: "css", + }; + }); + }, + }; +} diff --git a/packages/remix-dev/compiler/plugins/cssModulesPlugin.ts b/packages/remix-dev/compiler/plugins/cssModulesPlugin.ts index f21e031bac2..0fb698423c9 100644 --- a/packages/remix-dev/compiler/plugins/cssModulesPlugin.ts +++ b/packages/remix-dev/compiler/plugins/cssModulesPlugin.ts @@ -5,6 +5,8 @@ import postcss from "postcss"; import postcssModules from "postcss-modules"; import type { CompileOptions } from "../options"; +import type { RemixConfig } from "../../config"; +import { loadPostcssPlugins } from "../utils/postcss"; const pluginName = "css-modules-plugin"; const namespace = `${pluginName}-ns`; @@ -17,14 +19,20 @@ interface PluginData { compiledCss: string; } -export const cssModulesPlugin = (options: { +export const cssModulesPlugin = ({ + config, + mode, + outputCss, +}: { + config: RemixConfig; mode: CompileOptions["mode"]; - rootDirectory: string; outputCss: boolean; }): Plugin => { return { name: pluginName, setup: async (build: PluginBuild) => { + let postcssPlugins = await loadPostcssPlugins({ config }); + build.onResolve( { filter: cssModulesFilter, namespace: "file" }, async (args) => { @@ -49,9 +57,10 @@ export const cssModulesPlugin = (options: { let exports: Record = {}; let { css: compiledCss } = await postcss([ + ...postcssPlugins, postcssModules({ generateScopedName: - options.mode === "production" + mode === "production" ? "[hash:base64:5]" : "[name]__[local]__[hash:base64:5]", getJSON: function (_, json) { @@ -76,7 +85,7 @@ export const cssModulesPlugin = (options: { // object that maps local names to generated class names. The compiled // CSS file contents are passed to the virtual CSS file via pluginData. let contents = [ - options.outputCss + outputCss ? `import "./${path.basename(absolutePath)}${compiledCssQuery}";` : null, `export default ${JSON.stringify(exports)};`, @@ -102,7 +111,7 @@ export const cssModulesPlugin = (options: { return { namespace, - path: path.relative(options.rootDirectory, absolutePath), + path: path.relative(config.rootDirectory, absolutePath), pluginData, }; }); diff --git a/packages/remix-dev/compiler/plugins/cssSideEffectImportsPlugin.ts b/packages/remix-dev/compiler/plugins/cssSideEffectImportsPlugin.ts index 662fbb6503f..2af73b1df0b 100644 --- a/packages/remix-dev/compiler/plugins/cssSideEffectImportsPlugin.ts +++ b/packages/remix-dev/compiler/plugins/cssSideEffectImportsPlugin.ts @@ -6,6 +6,9 @@ import { parse, type ParserOptions } from "@babel/parser"; import traverse from "@babel/traverse"; import generate from "@babel/generator"; +import type { RemixConfig } from "../../config"; +import { getPostcssProcessor } from "../utils/postcss"; + const pluginName = "css-side-effects-plugin"; const namespace = `${pluginName}-ns`; const cssSideEffectSuffix = "?__remix_sideEffect__"; @@ -38,12 +41,16 @@ const loaderForExtension: Record = { * to the CSS bundle. This is primarily designed to support packages that * import plain CSS files directly within JS files. */ -export const cssSideEffectImportsPlugin = (options: { - rootDirectory: string; +export const cssSideEffectImportsPlugin = ({ + config, +}: { + config: RemixConfig; }): Plugin => { return { name: pluginName, setup: async (build) => { + let postcssProcessor = await getPostcssProcessor({ config }); + build.onLoad( { filter: allJsFilesFilter, namespace: "file" }, async (args) => { @@ -75,7 +82,7 @@ export const cssSideEffectImportsPlugin = (options: { ).path; return { - path: path.relative(options.rootDirectory, resolvedPath), + path: path.relative(config.rootDirectory, resolvedPath), namespace: resolvedPath.endsWith(".css") ? namespace : undefined, }; } @@ -84,6 +91,18 @@ export const cssSideEffectImportsPlugin = (options: { build.onLoad({ filter: /\.css$/, namespace }, async (args) => { let contents = await fse.readFile(args.path, "utf8"); + if (postcssProcessor) { + contents = ( + await postcssProcessor.process(contents, { + from: args.path, + to: args.path, + map: { + inline: true, + }, + }) + ).css; + } + return { contents, resolveDir: path.dirname(args.path), diff --git a/packages/remix-dev/compiler/plugins/vanillaExtractPlugin.ts b/packages/remix-dev/compiler/plugins/vanillaExtractPlugin.ts index 9af8a589972..04cfda834b6 100644 --- a/packages/remix-dev/compiler/plugins/vanillaExtractPlugin.ts +++ b/packages/remix-dev/compiler/plugins/vanillaExtractPlugin.ts @@ -13,6 +13,7 @@ import * as esbuild from "esbuild"; import type { RemixConfig } from "../../config"; import type { CompileOptions } from "../options"; import { loaders } from "../loaders"; +import { getPostcssProcessor } from "../utils/postcss"; const pluginName = "vanilla-extract-plugin"; const namespace = `${pluginName}-ns`; @@ -28,7 +29,11 @@ export function vanillaExtractPlugin({ }): esbuild.Plugin { return { name: pluginName, - setup(build) { + async setup(build) { + let postcssProcessor = await getPostcssProcessor({ + config, + vanillaExtract: true, + }); let { rootDirectory } = config; build.onResolve({ filter: virtualCssFileFilter }, (args) => { @@ -44,6 +49,15 @@ export function vanillaExtractPlugin({ let { source, fileName } = await getSourceFromVirtualCssFile(path); let resolveDir = dirname(join(rootDirectory, fileName)); + if (postcssProcessor) { + source = ( + await postcssProcessor.process(source, { + from: path, + to: path, + }) + ).css; + } + return { contents: source, loader: "css", diff --git a/packages/remix-dev/compiler/utils/postcss.ts b/packages/remix-dev/compiler/utils/postcss.ts new file mode 100644 index 00000000000..3a76c9be29e --- /dev/null +++ b/packages/remix-dev/compiler/utils/postcss.ts @@ -0,0 +1,129 @@ +import loadConfig from "postcss-load-config"; +import type { AcceptedPlugin, Processor } from "postcss"; +import postcss from "postcss"; + +import type { RemixConfig } from "../../config"; + +interface Options { + config: RemixConfig; + vanillaExtract?: boolean; +} + +function isPostcssEnabled(config: RemixConfig) { + return config.future.unstable_postcss || config.future.unstable_tailwind; +} + +function getCacheKey({ config, vanillaExtract }: Required) { + return [config.rootDirectory, vanillaExtract].join("|"); +} + +let pluginsCache = new Map>(); +export async function loadPostcssPlugins({ + config, + vanillaExtract = false, +}: Options): Promise> { + if (!isPostcssEnabled(config)) { + return []; + } + + let { rootDirectory } = config; + let cacheKey = getCacheKey({ config, vanillaExtract }); + let cachedPlugins = pluginsCache.get(cacheKey); + if (cachedPlugins) { + return cachedPlugins; + } + + let plugins: Array = []; + + if (config.future.unstable_postcss) { + try { + let context = { + remix: { + vanillaExtract, + }, + }; + + let postcssConfig = await loadConfig( + // @ts-expect-error Custom context extensions aren't type safe + context, + rootDirectory + ); + + plugins.push(...postcssConfig.plugins); + } catch (err) {} + } + + if (config.future.unstable_tailwind) { + let tailwindPlugin = await loadTailwindPlugin(config); + if (tailwindPlugin && !hasTailwindPlugin(plugins)) { + plugins.push(tailwindPlugin); + } + } + + pluginsCache.set(cacheKey, plugins); + return plugins; +} + +let processorCache = new Map(); +export async function getPostcssProcessor({ + config, + vanillaExtract = false, +}: Options): Promise { + if (!isPostcssEnabled(config)) { + return null; + } + + let cacheKey = getCacheKey({ config, vanillaExtract }); + let cachedProcessor = processorCache.get(cacheKey); + if (cachedProcessor !== undefined) { + return cachedProcessor; + } + + let plugins = await loadPostcssPlugins({ config, vanillaExtract }); + let processor = plugins.length > 0 ? postcss(plugins) : null; + + processorCache.set(cacheKey, processor); + return processor; +} + +function hasTailwindPlugin(plugins: Array) { + return plugins.some( + (plugin) => + "postcssPlugin" in plugin && plugin.postcssPlugin === "tailwindcss" + ); +} + +let tailwindPluginCache = new Map(); +async function loadTailwindPlugin( + config: RemixConfig +): Promise { + let { rootDirectory } = config; + let cacheKey = rootDirectory; + let cachedTailwindPlugin = tailwindPluginCache.get(cacheKey); + if (cachedTailwindPlugin !== undefined) { + return cachedTailwindPlugin; + } + + let tailwindPlugin: AcceptedPlugin | null = null; + + try { + // First ensure we have a Tailwind config + require.resolve("./tailwind.config", { paths: [rootDirectory] }); + + // Load Tailwind from the project directory + let tailwindPath = require.resolve("tailwindcss", { + paths: [rootDirectory], + }); + + let importedTailwindPlugin = (await import(tailwindPath))?.default; + + // Check that it declares itself as a PostCSS plugin + if (importedTailwindPlugin && importedTailwindPlugin.postcss) { + tailwindPlugin = importedTailwindPlugin; + } + } catch (err) {} + + tailwindPluginCache.set(cacheKey, tailwindPlugin); + + return tailwindPlugin; +} diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index 1733a292604..a8b32a9f728 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -35,6 +35,8 @@ export type ServerPlatform = "node" | "neutral"; interface FutureConfig { unstable_cssModules: boolean; unstable_cssSideEffectImports: boolean; + unstable_postcss: boolean; + unstable_tailwind: boolean; unstable_vanillaExtract: boolean; v2_meta: boolean; v2_routeConvention: boolean; @@ -494,6 +496,8 @@ export async function readConfig( unstable_cssModules: appConfig.future?.unstable_cssModules === true, unstable_cssSideEffectImports: appConfig.future?.unstable_cssSideEffectImports === true, + unstable_postcss: appConfig.future?.unstable_postcss === true, + unstable_tailwind: appConfig.future?.unstable_tailwind === true, unstable_vanillaExtract: appConfig.future?.unstable_vanillaExtract === true, v2_meta: appConfig.future?.v2_meta === true, v2_routeConvention: appConfig.future?.v2_routeConvention === true, diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index f0e6465598d..1e941ef9e35 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -56,6 +56,7 @@ "ora": "^5.4.1", "postcss": "^8.4.19", "postcss-discard-duplicates": "^5.1.0", + "postcss-load-config": "^4.0.1", "postcss-modules": "^6.0.0", "prettier": "2.7.1", "pretty-ms": "^7.0.1", diff --git a/packages/remix-react/entry.ts b/packages/remix-react/entry.ts index 77f17b4988b..4f6d28a88ea 100644 --- a/packages/remix-react/entry.ts +++ b/packages/remix-react/entry.ts @@ -20,6 +20,8 @@ export interface EntryContext extends RemixContextObject { export interface FutureConfig { unstable_cssModules: boolean; unstable_cssSideEffectImports: boolean; + unstable_postcss: boolean; + unstable_tailwind: boolean; unstable_vanillaExtract: boolean; v2_meta: boolean; } diff --git a/packages/remix-server-runtime/entry.ts b/packages/remix-server-runtime/entry.ts index 152bf2ad13a..cb76319efe5 100644 --- a/packages/remix-server-runtime/entry.ts +++ b/packages/remix-server-runtime/entry.ts @@ -14,6 +14,8 @@ export interface EntryContext { export interface FutureConfig { unstable_cssModules: true; unstable_cssSideEffectImports: boolean; + unstable_postcss: boolean; + unstable_tailwind: boolean; unstable_vanillaExtract: boolean; v2_meta: boolean; } diff --git a/packages/remix-testing/create-remix-stub.tsx b/packages/remix-testing/create-remix-stub.tsx index fef6ea3021b..549d13976a1 100644 --- a/packages/remix-testing/create-remix-stub.tsx +++ b/packages/remix-testing/create-remix-stub.tsx @@ -71,6 +71,8 @@ export function createRemixStub(routes: AgnosticDataRouteObject[]) { v2_meta: false, unstable_cssModules: false, unstable_cssSideEffectImports: false, + unstable_postcss: false, + unstable_tailwind: false, unstable_vanillaExtract: false, ...remixConfigFuture, }, diff --git a/yarn.lock b/yarn.lock index 40defc812a5..ed40a28cdf4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3364,7 +3364,16 @@ acorn-jsx@^5.0.0, acorn-jsx@^5.3.2: resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn-walk@^7.1.1: +acorn-node@^1.8.2: + version "1.8.2" + resolved "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8" + integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A== + dependencies: + acorn "^7.0.0" + acorn-walk "^7.0.0" + xtend "^4.0.2" + +acorn-walk@^7.0.0, acorn-walk@^7.1.1: version "7.2.0" resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== @@ -3374,7 +3383,7 @@ acorn-walk@^8.2.0: resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== -acorn@^7.1.1: +acorn@^7.0.0, acorn@^7.1.1: version "7.4.1" resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== @@ -3723,6 +3732,11 @@ arg@^5.0.1: resolved "https://registry.npmjs.org/arg/-/arg-5.0.1.tgz" integrity sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA== +arg@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" + integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== + argparse@^1.0.7: version "1.0.10" resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" @@ -4358,6 +4372,11 @@ callsites@^3.0.0: resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== +camelcase-css@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" + integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== + camelcase-keys@^6.2.2: version "6.2.2" resolved "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz" @@ -4508,9 +4527,9 @@ choices-separator@^2.0.0: debug "^2.6.6" strip-color "^0.1.0" -chokidar@^3.4.2, chokidar@^3.5.1: +chokidar@^3.4.2, chokidar@^3.5.1, chokidar@^3.5.3: version "3.5.3" - resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" + resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== dependencies: anymatch "~3.1.2" @@ -4693,7 +4712,7 @@ color-name@1.1.3: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@~1.1.4: +color-name@^1.1.4, color-name@~1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== @@ -5205,6 +5224,11 @@ define-property@^2.0.2: is-descriptor "^1.0.2" isobject "^3.0.1" +defined@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz#c0b9db27bfaffd95d6f61399419b893df0f91ebf" + integrity sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q== + degenerator@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/degenerator/-/degenerator-3.0.2.tgz" @@ -5255,6 +5279,20 @@ detect-newline@3.1.0, detect-newline@^3.0.0: resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== +detective@^5.2.1: + version "5.2.1" + resolved "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz#6af01eeda11015acb0e73f933242b70f24f91034" + integrity sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw== + dependencies: + acorn-node "^1.8.2" + defined "^1.0.0" + minimist "^1.2.6" + +didyoumean@^1.2.2: + version "1.2.2" + resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" + integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== + diff-sequences@^27.5.1: version "27.5.1" resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz" @@ -5277,6 +5315,11 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" +dlv@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" + integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== + doctrine@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz" @@ -6309,9 +6352,9 @@ fast-glob@3.2.11, fast-glob@^3.0.3, fast-glob@^3.2.7, fast-glob@^3.2.9: merge2 "^1.3.0" micromatch "^4.0.4" -fast-glob@^3.2.11: +fast-glob@^3.2.11, fast-glob@^3.2.12: version "3.2.12" - resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz" + resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== dependencies: "@nodelib/fs.stat" "^2.0.2" @@ -6745,7 +6788,7 @@ glob-parent@^5.1.2, glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" -glob-parent@^6.0.1: +glob-parent@^6.0.1, glob-parent@^6.0.2: version "6.0.2" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== @@ -8567,6 +8610,11 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +lilconfig@^2.0.5, lilconfig@^2.0.6: + version "2.0.6" + resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz#32a384558bd58af3d4c6e077dd1ad1d397bc69d4" + integrity sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg== + lines-and-columns@^1.1.6: version "1.1.6" resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz" @@ -9480,7 +9528,7 @@ micromatch@^3.1.10: snapdragon "^0.8.1" to-regex "^3.0.2" -micromatch@^4.0.2: +micromatch@^4.0.2, micromatch@^4.0.5: version "4.0.5" resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz" integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== @@ -9886,6 +9934,11 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" +object-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== + object-inspect@^1.11.0, object-inspect@^1.9.0: version "1.11.0" resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz" @@ -10370,7 +10423,7 @@ pidtree@^0.3.0: resolved "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz" integrity sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA== -pify@^2.2.0: +pify@^2.2.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= @@ -10424,6 +10477,38 @@ postcss-discard-duplicates@^5.1.0: resolved "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz#9eb4fe8456706a4eebd6d3b7b777d07bad03e848" integrity sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw== +postcss-import@^14.1.0: + version "14.1.0" + resolved "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz#a7333ffe32f0b8795303ee9e40215dac922781f0" + integrity sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw== + dependencies: + postcss-value-parser "^4.0.0" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-js@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz#31db79889531b80dc7bc9b0ad283e418dce0ac00" + integrity sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ== + dependencies: + camelcase-css "^2.0.1" + +postcss-load-config@^3.1.4: + version "3.1.4" + resolved "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855" + integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg== + dependencies: + lilconfig "^2.0.5" + yaml "^1.10.2" + +postcss-load-config@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz#152383f481c2758274404e4962743191d73875bd" + integrity sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA== + dependencies: + lilconfig "^2.0.5" + yaml "^2.1.1" + postcss-modules-extract-imports@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" @@ -10466,7 +10551,14 @@ postcss-modules@^6.0.0: postcss-modules-values "^4.0.0" string-hash "^1.1.1" -postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: +postcss-nested@6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz#1572f1984736578f360cffc7eb7dca69e30d1735" + integrity sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w== + dependencies: + postcss-selector-parser "^6.0.10" + +postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: version "6.0.11" resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc" integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g== @@ -10474,11 +10566,20 @@ postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: cssesc "^3.0.0" util-deprecate "^1.0.2" -postcss-value-parser@^4.1.0: +postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== +postcss@^8.4.18: + version "8.4.21" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4" + integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg== + dependencies: + nanoid "^3.3.4" + picocolors "^1.0.0" + source-map-js "^1.0.2" + postcss@^8.4.19: version "8.4.19" resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz#61178e2add236b17351897c8bcc0b4c8ecab56fc" @@ -10822,6 +10923,13 @@ react@^18.2.0: dependencies: loose-envify "^1.1.0" +read-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" + integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA== + dependencies: + pify "^2.3.0" + read-pkg-up@^7.0.1: version "7.0.1" resolved "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz" @@ -11189,9 +11297,9 @@ resolve@^1.1.6, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.14.2, resolve@^1.19 path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@^1.22.0: +resolve@^1.1.7, resolve@^1.22.0, resolve@^1.22.1: version "1.22.1" - resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== dependencies: is-core-module "^2.9.0" @@ -12089,6 +12197,35 @@ synckit@^0.8.3: "@pkgr/utils" "^2.3.1" tslib "^2.4.0" +tailwindcss@^3.1.8: + version "3.2.4" + resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz#afe3477e7a19f3ceafb48e4b083e292ce0dc0250" + integrity sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ== + dependencies: + arg "^5.0.2" + chokidar "^3.5.3" + color-name "^1.1.4" + detective "^5.2.1" + didyoumean "^1.2.2" + dlv "^1.1.3" + fast-glob "^3.2.12" + glob-parent "^6.0.2" + is-glob "^4.0.3" + lilconfig "^2.0.6" + micromatch "^4.0.5" + normalize-path "^3.0.0" + object-hash "^3.0.0" + picocolors "^1.0.0" + postcss "^8.4.18" + postcss-import "^14.1.0" + postcss-js "^4.0.0" + postcss-load-config "^3.1.4" + postcss-nested "6.0.0" + postcss-selector-parser "^6.0.10" + postcss-value-parser "^4.2.0" + quick-lru "^5.1.1" + resolve "^1.22.1" + tapable@^2.2.0: version "2.2.1" resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz" @@ -13186,9 +13323,9 @@ xregexp@2.0.0: resolved "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz" integrity sha512-xl/50/Cf32VsGq/1R8jJE5ajH1yMCQkpmoS10QbFZWl2Oor4H0Me64Pu2yxvsRWK3m6soJbmGfzSR7BYmDcWAA== -xtend@~4.0.1: +xtend@^4.0.2, xtend@~4.0.1: version "4.0.2" - resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz" + resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== y18n@^4.0.0: @@ -13221,6 +13358,11 @@ yaml@^1.10.2: resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +yaml@^2.1.1: + version "2.2.1" + resolved "https://registry.npmjs.org/yaml/-/yaml-2.2.1.tgz#3014bf0482dcd15147aa8e56109ce8632cd60ce4" + integrity sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw== + yargs-parser@^18.1.2, yargs-parser@^18.1.3: version "18.1.3" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz" From 46f2a7ab607f123c62dea72c3ee9bd3c2bc7b8b3 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Tue, 24 Jan 2023 09:30:13 +1100 Subject: [PATCH 02/17] tidy up source maps, handle build source maps --- packages/remix-dev/compiler/compileBrowser.ts | 15 ++++------ packages/remix-dev/compiler/compilerServer.ts | 8 +++--- .../compiler/plugins/cssFilePlugin.ts | 28 ++++++++++--------- .../plugins/cssSideEffectImportsPlugin.ts | 7 +++-- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/packages/remix-dev/compiler/compileBrowser.ts b/packages/remix-dev/compiler/compileBrowser.ts index d1957f25d56..096941170d6 100644 --- a/packages/remix-dev/compiler/compileBrowser.ts +++ b/packages/remix-dev/compiler/compileBrowser.ts @@ -113,9 +113,9 @@ const createEsbuildConfig = ( ? vanillaExtractPlugin({ config, mode, outputCss }) : null, config.future.unstable_cssSideEffectImports - ? cssSideEffectImportsPlugin({ config }) + ? cssSideEffectImportsPlugin({ config, options }) : null, - cssFilePlugin({ config, mode, sourcemap }), + cssFilePlugin({ config, options }), urlImportsPlugin(), mdxPlugin(config), browserRouteModulesPlugin(config, /\?browser$/), @@ -234,11 +234,6 @@ export const createBrowserCompiler = ( let cssBundlePath = cssBundleFile.path; - // Get esbuild's existing CSS source map so we can pass it to PostCSS - let cssBundleSourceMap = outputFiles.find((outputFile) => - isCssBundleFile(outputFile, ".css.map") - )?.text; - let { css, map } = await postcss([ // We need to discard duplicate rules since "composes" // in CSS Modules can result in duplicate styles @@ -246,8 +241,10 @@ export const createBrowserCompiler = ( ]).process(cssBundleFile.text, { from: cssBundlePath, to: cssBundlePath, - map: { - prev: cssBundleSourceMap, + map: options.sourcemap && { + prev: outputFiles.find((outputFile) => + isCssBundleFile(outputFile, ".css.map") + )?.text, inline: false, annotation: false, sourcesContent: true, diff --git a/packages/remix-dev/compiler/compilerServer.ts b/packages/remix-dev/compiler/compilerServer.ts index bdf4935c64d..c01812626bf 100644 --- a/packages/remix-dev/compiler/compilerServer.ts +++ b/packages/remix-dev/compiler/compilerServer.ts @@ -50,7 +50,7 @@ const createEsbuildConfig = ( ); let isDenoRuntime = config.serverBuildTarget === "deno"; - let { mode, sourcemap } = options; + let { mode } = options; let outputCss = false; let plugins: esbuild.Plugin[] = [ @@ -62,9 +62,9 @@ const createEsbuildConfig = ( ? vanillaExtractPlugin({ config, mode, outputCss }) : null, config.future.unstable_cssSideEffectImports - ? cssSideEffectImportsPlugin({ config }) + ? cssSideEffectImportsPlugin({ config, options }) : null, - cssFilePlugin({ config, mode, sourcemap }), + cssFilePlugin({ config, options }), urlImportsPlugin(), mdxPlugin(config), emptyModulesPlugin(config, /\.client(\.[jt]sx?)?$/), @@ -113,7 +113,7 @@ const createEsbuildConfig = ( // `undefined`, esbuild will keep looking for a tsconfig.json recursively up. This unwanted // behavior can only be avoided by creating an empty tsconfig file in the root directory. tsconfig: config.tsconfigPath, - sourcemap, // use linked (true) to fix up .map file + sourcemap: options.sourcemap, // use linked (true) to fix up .map file // The server build needs to know how to generate asset URLs for imports // of CSS and other files. assetNames: "_assets/[name]-[hash]", diff --git a/packages/remix-dev/compiler/plugins/cssFilePlugin.ts b/packages/remix-dev/compiler/plugins/cssFilePlugin.ts index 863f7f1b9bc..4e6ab128976 100644 --- a/packages/remix-dev/compiler/plugins/cssFilePlugin.ts +++ b/packages/remix-dev/compiler/plugins/cssFilePlugin.ts @@ -20,12 +20,10 @@ function normalizePathSlashes(p: string) { */ export function cssFilePlugin({ config, - mode, - sourcemap, + options, }: { config: RemixConfig; - mode: CompileOptions["mode"]; - sourcemap: CompileOptions["sourcemap"]; + options: CompileOptions; }): esbuild.Plugin { return { name: "css-file", @@ -38,12 +36,11 @@ export function cssFilePlugin({ build.onLoad({ filter: /\.css$/ }, async (args) => { let { metafile, outputFiles, warnings, errors } = await esbuild.build({ ...buildOps, - absWorkingDir: config.rootDirectory, - minify: mode === "production", + minify: options.mode === "production", minifySyntax: true, metafile: true, write: false, - sourcemap, + sourcemap: Boolean(options.sourcemap && postcssProcessor), // If we're not running PostCSS, we're not transforming CSS so we don't need source maps incremental: false, splitting: false, stdin: undefined, @@ -71,7 +68,9 @@ export function cssFilePlugin({ }); }, }, - ...(postcssProcessor ? [postcssPlugin(postcssProcessor)] : []), + ...(postcssProcessor + ? [postcssPlugin({ postcssProcessor, options })] + : []), ], }); @@ -131,7 +130,13 @@ export function cssFilePlugin({ }; } -function postcssPlugin(postcssProcessor: Processor): esbuild.Plugin { +function postcssPlugin({ + postcssProcessor, + options, +}: { + postcssProcessor: Processor; + options: CompileOptions; +}): esbuild.Plugin { return { name: "postcss-plugin", async setup(build) { @@ -142,10 +147,7 @@ function postcssPlugin(postcssProcessor: Processor): esbuild.Plugin { await postcssProcessor.process(contents, { from: args.path, to: args.path, - map: { - inline: true, - sourcesContent: true, - }, + map: options.sourcemap, }) ).css; diff --git a/packages/remix-dev/compiler/plugins/cssSideEffectImportsPlugin.ts b/packages/remix-dev/compiler/plugins/cssSideEffectImportsPlugin.ts index 2af73b1df0b..ac7c5909919 100644 --- a/packages/remix-dev/compiler/plugins/cssSideEffectImportsPlugin.ts +++ b/packages/remix-dev/compiler/plugins/cssSideEffectImportsPlugin.ts @@ -7,6 +7,7 @@ import traverse from "@babel/traverse"; import generate from "@babel/generator"; import type { RemixConfig } from "../../config"; +import type { CompileOptions } from "../options"; import { getPostcssProcessor } from "../utils/postcss"; const pluginName = "css-side-effects-plugin"; @@ -43,8 +44,10 @@ const loaderForExtension: Record = { */ export const cssSideEffectImportsPlugin = ({ config, + options, }: { config: RemixConfig; + options: CompileOptions; }): Plugin => { return { name: pluginName, @@ -96,9 +99,7 @@ export const cssSideEffectImportsPlugin = ({ await postcssProcessor.process(contents, { from: args.path, to: args.path, - map: { - inline: true, - }, + map: options.sourcemap, }) ).css; } From 7fb786d156ad730a4d9eb97ce075b805900fb593 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Tue, 24 Jan 2023 09:36:40 +1100 Subject: [PATCH 03/17] fix tagged template literal --- integration/postcss-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/postcss-test.ts b/integration/postcss-test.ts index 31c149ce03b..b7bf3b946d6 100644 --- a/integration/postcss-test.ts +++ b/integration/postcss-test.ts @@ -230,7 +230,7 @@ test.describe("PostCSS", () => { ); } `, - "app/test-components/vanilla-extract/styles.css.ts": css` + "app/test-components/vanilla-extract/styles.css.ts": js` import { style, globalStyle } from "@vanilla-extract/css"; export const root = style({ From 18209df23d0e5a5d5eb620aae8966c74a22d611c Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Tue, 24 Jan 2023 09:38:25 +1100 Subject: [PATCH 04/17] nit comment --- packages/remix-dev/compiler/plugins/cssFilePlugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/remix-dev/compiler/plugins/cssFilePlugin.ts b/packages/remix-dev/compiler/plugins/cssFilePlugin.ts index 4e6ab128976..cd3fe4aab99 100644 --- a/packages/remix-dev/compiler/plugins/cssFilePlugin.ts +++ b/packages/remix-dev/compiler/plugins/cssFilePlugin.ts @@ -40,7 +40,7 @@ export function cssFilePlugin({ minifySyntax: true, metafile: true, write: false, - sourcemap: Boolean(options.sourcemap && postcssProcessor), // If we're not running PostCSS, we're not transforming CSS so we don't need source maps + sourcemap: Boolean(options.sourcemap && postcssProcessor), // We only need source maps if we're processing the CSS with PostCSS incremental: false, splitting: false, stdin: undefined, From 83ca6dd860894cf6b83cfd560ce7fa1deaebf86b Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Tue, 24 Jan 2023 10:09:38 +1100 Subject: [PATCH 05/17] refactor PostCSS context --- .../compiler/plugins/vanillaExtractPlugin.ts | 4 ++- packages/remix-dev/compiler/utils/postcss.ts | 32 ++++++++++--------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/remix-dev/compiler/plugins/vanillaExtractPlugin.ts b/packages/remix-dev/compiler/plugins/vanillaExtractPlugin.ts index 04cfda834b6..cc98042159e 100644 --- a/packages/remix-dev/compiler/plugins/vanillaExtractPlugin.ts +++ b/packages/remix-dev/compiler/plugins/vanillaExtractPlugin.ts @@ -32,7 +32,9 @@ export function vanillaExtractPlugin({ async setup(build) { let postcssProcessor = await getPostcssProcessor({ config, - vanillaExtract: true, + context: { + vanillaExtract: true, + }, }); let { rootDirectory } = config; diff --git a/packages/remix-dev/compiler/utils/postcss.ts b/packages/remix-dev/compiler/utils/postcss.ts index 3a76c9be29e..3257466b87a 100644 --- a/packages/remix-dev/compiler/utils/postcss.ts +++ b/packages/remix-dev/compiler/utils/postcss.ts @@ -4,30 +4,38 @@ import postcss from "postcss"; import type { RemixConfig } from "../../config"; +interface RemixPostcssContext { + vanillaExtract: boolean; +} + +const defaultContext: RemixPostcssContext = { + vanillaExtract: false, +}; + interface Options { config: RemixConfig; - vanillaExtract?: boolean; + context?: RemixPostcssContext; } function isPostcssEnabled(config: RemixConfig) { return config.future.unstable_postcss || config.future.unstable_tailwind; } -function getCacheKey({ config, vanillaExtract }: Required) { - return [config.rootDirectory, vanillaExtract].join("|"); +function getCacheKey({ config, context }: Required) { + return [config.rootDirectory, context.vanillaExtract].join("|"); } let pluginsCache = new Map>(); export async function loadPostcssPlugins({ config, - vanillaExtract = false, + context = defaultContext, }: Options): Promise> { if (!isPostcssEnabled(config)) { return []; } let { rootDirectory } = config; - let cacheKey = getCacheKey({ config, vanillaExtract }); + let cacheKey = getCacheKey({ config, context }); let cachedPlugins = pluginsCache.get(cacheKey); if (cachedPlugins) { return cachedPlugins; @@ -37,15 +45,9 @@ export async function loadPostcssPlugins({ if (config.future.unstable_postcss) { try { - let context = { - remix: { - vanillaExtract, - }, - }; - let postcssConfig = await loadConfig( // @ts-expect-error Custom context extensions aren't type safe - context, + { remix: context }, rootDirectory ); @@ -67,19 +69,19 @@ export async function loadPostcssPlugins({ let processorCache = new Map(); export async function getPostcssProcessor({ config, - vanillaExtract = false, + context = defaultContext, }: Options): Promise { if (!isPostcssEnabled(config)) { return null; } - let cacheKey = getCacheKey({ config, vanillaExtract }); + let cacheKey = getCacheKey({ config, context }); let cachedProcessor = processorCache.get(cacheKey); if (cachedProcessor !== undefined) { return cachedProcessor; } - let plugins = await loadPostcssPlugins({ config, vanillaExtract }); + let plugins = await loadPostcssPlugins({ config, context }); let processor = plugins.length > 0 ? postcss(plugins) : null; processorCache.set(cacheKey, processor); From 3b0a08b5fcf0a98d85082cc97f9f1f6a652e8ceb Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Tue, 24 Jan 2023 10:19:03 +1100 Subject: [PATCH 06/17] tidy up sourcemap option --- packages/remix-dev/compiler/compileBrowser.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/remix-dev/compiler/compileBrowser.ts b/packages/remix-dev/compiler/compileBrowser.ts index 096941170d6..39c65ef4174 100644 --- a/packages/remix-dev/compiler/compileBrowser.ts +++ b/packages/remix-dev/compiler/compileBrowser.ts @@ -98,7 +98,7 @@ const createEsbuildConfig = ( } } - let { mode, sourcemap } = options; + let { mode } = options; let outputCss = isCssBuild; let plugins: esbuild.Plugin[] = [ @@ -143,7 +143,7 @@ const createEsbuildConfig = ( bundle: true, logLevel: "silent", splitting: !isCssBuild, - sourcemap, + sourcemap: options.sourcemap, // As pointed out by https://github.com/evanw/esbuild/issues/2440, when tsconfig is set to // `undefined`, esbuild will keep looking for a tsconfig.json recursively up. This unwanted // behavior can only be avoided by creating an empty tsconfig file in the root directory. From 9aaaf861db1e261940dfb0f90d66c117a0a5eff2 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Tue, 24 Jan 2023 10:22:38 +1100 Subject: [PATCH 07/17] refactor, tweak comment --- packages/remix-dev/compiler/utils/postcss.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/remix-dev/compiler/utils/postcss.ts b/packages/remix-dev/compiler/utils/postcss.ts index 3257466b87a..2775c36d9af 100644 --- a/packages/remix-dev/compiler/utils/postcss.ts +++ b/packages/remix-dev/compiler/utils/postcss.ts @@ -4,6 +4,11 @@ import postcss from "postcss"; import type { RemixConfig } from "../../config"; +interface Options { + config: RemixConfig; + context?: RemixPostcssContext; +} + interface RemixPostcssContext { vanillaExtract: boolean; } @@ -12,11 +17,6 @@ const defaultContext: RemixPostcssContext = { vanillaExtract: false, }; -interface Options { - config: RemixConfig; - context?: RemixPostcssContext; -} - function isPostcssEnabled(config: RemixConfig) { return config.future.unstable_postcss || config.future.unstable_tailwind; } @@ -46,7 +46,9 @@ export async function loadPostcssPlugins({ if (config.future.unstable_postcss) { try { let postcssConfig = await loadConfig( - // @ts-expect-error Custom context extensions aren't type safe + // We're nesting our custom context values in a "remix" + // namespace to avoid clashing with other tools. + // @ts-expect-error Custom context values aren't type safe. { remix: context }, rootDirectory ); From 3c6e22d3f0c3bd83acb3e90e4ae49b5505c77163 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Tue, 24 Jan 2023 10:41:01 +1100 Subject: [PATCH 08/17] add Vanilla Extract + Tailwind test cases --- integration/tailwind-test.ts | 84 +++++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/integration/tailwind-test.ts b/integration/tailwind-test.ts index a3cf4084f19..5fca3adcc90 100644 --- a/integration/tailwind-test.ts +++ b/integration/tailwind-test.ts @@ -71,8 +71,10 @@ test.describe("Tailwind", () => { `, ...basicUsageFixture(), ...regularStylesSheetsFixture(), - ...cssSideEffectsFixture(), ...cssModulesFixture(), + ...vanillaExtractClassCompositionFixture(), + ...vanillaExtractTailwindFunctionsFixture(), + ...cssSideEffectsFixture(), }, }); appFixture = await createAppFixture(fixture); @@ -181,6 +183,86 @@ test.describe("Tailwind", () => { expect(padding).toBe(TEST_PADDING_VALUE); }); + let vanillaExtractClassCompositionFixture = () => ({ + "app/routes/vanilla-extract-class-composition-test.jsx": js` + import { Test } from "~/test-components/vanilla-extract-class-composition"; + + export default function() { + return ; + } + `, + "app/test-components/vanilla-extract-class-composition/index.jsx": js` + import * as styles from "./styles.css"; + + export function Test() { + return ( +
+ Vanilla Extract test +
+ ); + } + `, + "app/test-components/vanilla-extract-class-composition/styles.css.ts": js` + import { style } from "@vanilla-extract/css"; + + export const root = style([ + { background: 'peachpuff' }, + "p-test", + ]); + `, + }); + test("Vanilla Extract class composition", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/vanilla-extract-class-composition-test"); + let locator = await page.locator( + "[data-testid='vanilla-extract-class-composition']" + ); + let padding = await locator.evaluate( + (element) => window.getComputedStyle(element).padding + ); + expect(padding).toBe(TEST_PADDING_VALUE); + }); + + let vanillaExtractTailwindFunctionsFixture = () => ({ + "app/routes/vanilla-extract-tailwind-functions-test.jsx": js` + import { Test } from "~/test-components/vanilla-extract-tailwind-functions"; + + export default function() { + return ; + } + `, + "app/test-components/vanilla-extract-tailwind-functions/index.jsx": js` + import * as styles from "./styles.css"; + + export function Test() { + return ( +
+ Vanilla Extract Tailwind functions test +
+ ); + } + `, + "app/test-components/vanilla-extract-tailwind-functions/styles.css.ts": js` + import { style } from "@vanilla-extract/css"; + + export const root = style({ + background: 'peachpuff', + padding: 'theme(spacing.test)', + }); + `, + }); + test("Vanilla Extract Tailwind functions", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/vanilla-extract-tailwind-functions-test"); + let locator = await page.locator( + "[data-testid='vanilla-extract-tailwind-functions']" + ); + let padding = await locator.evaluate( + (element) => window.getComputedStyle(element).padding + ); + expect(padding).toBe(TEST_PADDING_VALUE); + }); + let cssSideEffectsFixture = () => ({ "app/routes/css-side-effect-imports-test.jsx": js` import { Test } from "~/test-components/css-side-effect-imports"; From 6932068781723562724d237c49bda3532f8cfc24 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Tue, 24 Jan 2023 11:20:25 +1100 Subject: [PATCH 09/17] tweak docs, add PostCSS context docs --- docs/guides/styling.md | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/docs/guides/styling.md b/docs/guides/styling.md index cdaef2824ef..14bf811a6f0 100644 --- a/docs/guides/styling.md +++ b/docs/guides/styling.md @@ -402,7 +402,7 @@ export function links() { Perhaps the most popular way to style a Remix application in the community is to use [Tailwind CSS][tailwind]. It has the benefits of inline-style collocation for developer ergonomics and is able to generate a CSS file for Remix to import. The generated CSS file generally caps out around 8-10kb, even for large applications. Load that file into the `root.tsx` links and be done with it. If you don't have any CSS opinions, this is a great approach. -There are a couple of options for integrating Tailwind into Remix. You can leverage the new, experimental built-in support, or integrate Tailwind manually using their CLI. +There are a couple of options for integrating Tailwind into your Remix application. You can use Remix's built-in support, or integrate Tailwind manually using their CLI. ### Built-in Tailwind Support @@ -467,7 +467,7 @@ export const links: LinksFunction = () => [ ]; ``` -With this setup in place, you can also use any of [Tailwind's functions and directives][tailwind-functions-and-directives] anywhere in your CSS. +With this setup in place, you can also use [Tailwind's functions and directives][tailwind-functions-and-directives] anywhere in your CSS. Note that if you're also using Remix's [built-in PostCSS support](#built-in-postcss-support), the Tailwind PostCSS plugin will be automatically included if it's missing. @@ -602,7 +602,7 @@ export const links: LinksFunction = () => { [PostCSS][postcss] is a popular tool with a rich plugin ecosystem, commonly used to prefix CSS for older browsers, transpile future CSS syntax, inline images, lint your styles and more. -There are a couple of options for integrating PostCSS into Remix. You can leverage the new, experimental built-in support, or integrate PostCSS manually using their CLI. +There are a couple of options for integrating PostCSS into your Remix application. You can use Remix's built-in support, or integrate PostCSS manually using their CLI. ### Built-in PostCSS Support @@ -622,13 +622,13 @@ When a PostCSS config is detected, PostCSS will automatically be run across all }; ``` -2. Install the dependency: +2. Install any desired PostCSS plugins. ```sh npm install -D autoprefixer ``` -3. Add `postcss.config.js` in the Remix root referencing the plugin: +3. Add `postcss.config.js` in the Remix root with configuration for your plugins. ```js filename=postcss.config.js module.exports = { @@ -638,23 +638,38 @@ When a PostCSS config is detected, PostCSS will automatically be run across all }; ``` +If you're using [Vanilla Extract](#vanilla-extract), since it's already playing the role of CSS preprocessor, you may want to apply a different set of PostCSS plugins relative to other styles. To support this, you can export a function from `postcss.config.js` which is given a context object that lets you know when Remix is processing a Vanilla Extract file. + +```js filename=postcss.config.js +module.exports = (ctx) => { + return ctx.remix?.vanillaExtract + ? { + // PostCSS plugins for Vanilla Extract styles... + } + : { + // PostCSS plugins for other styles... + }; +}; + +``` + ### Manual PostCSS Integration It's also possible to use PostCSS without leveraging the built-in support. Here's the gist of it: -1. Use `postcss` cli directly alongside Remix +1. Use the `postcss` CLI directly alongside Remix 2. Build CSS into the Remix app directory from a styles source directory 3. Import your stylesheet to your modules like any other stylesheet Here's how to set it up: -1. Install the dev dependencies in your app: +1. Install PostCSS along with its CLI and any desired plugins in your app. ```sh npm install -D postcss-cli postcss autoprefixer ``` -2. Add `postcss.config.js` in the Remix root. +2. Add `postcss.config.js` in the Remix root with configuration for your plugins. ```js filename=postcss.config.js module.exports = { From ffc26bfd86d8ccd3201c1ad22fdfbff64617bbc8 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Tue, 24 Jan 2023 11:41:14 +1100 Subject: [PATCH 10/17] tweak docs --- docs/guides/styling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/styling.md b/docs/guides/styling.md index 14bf811a6f0..6a998ec1452 100644 --- a/docs/guides/styling.md +++ b/docs/guides/styling.md @@ -608,7 +608,7 @@ There are a couple of options for integrating PostCSS into your Remix applicatio This feature is unstable and currently only available behind a feature flag. We're confident in the use cases it solves but the API and implementation may change in the future. -When a PostCSS config is detected, PostCSS will automatically be run across all CSS in your project. For example, to use [Autoprefixer][autoprefixer]: +When a PostCSS config is detected, Remix will automatically run PostCSS across all CSS in your project. For example, to use [Autoprefixer][autoprefixer]: 1. Enable built-in PostCSS support by setting the the `future.unstable_postcss` feature flag in `remix.config.js`. From 5494b0ccacdbdf235840bcf140b93876c6282772 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Tue, 24 Jan 2023 11:41:22 +1100 Subject: [PATCH 11/17] add PostCSS test comment --- integration/postcss-test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/integration/postcss-test.ts b/integration/postcss-test.ts index b7bf3b946d6..a996602f425 100644 --- a/integration/postcss-test.ts +++ b/integration/postcss-test.ts @@ -47,6 +47,10 @@ test.describe("PostCSS", () => { }, }; `, + // We provide a test plugin that replaces the strings + // "TEST_PADDING_VALUE" and "TEST_POSTCSS_CONTEXT". + // This lets us assert that the plugin is being run + // and that the correct context values are provided. "postcss.config.js": js` module.exports = (ctx) => ({ plugins: [ From 77c0e3cb207a8b79219599e75824f4446e0a6886 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Tue, 24 Jan 2023 11:46:21 +1100 Subject: [PATCH 12/17] assert entire contents of PostCSS context --- integration/postcss-test.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/integration/postcss-test.ts b/integration/postcss-test.ts index a996602f425..16daaeeb16a 100644 --- a/integration/postcss-test.ts +++ b/integration/postcss-test.ts @@ -166,7 +166,9 @@ test.describe("PostCSS", () => { await app.goto("/regular-style-sheets-test"); let testId = "regular-style-sheets-postcss-context"; let postcssContext = await jsonFromBase64CssContent({ page, testId }); - expect(postcssContext.remix.vanillaExtract).toBe(false); + expect(postcssContext.remix).toEqual({ + vanillaExtract: false, + }); }); let cssModulesFixture = () => ({ @@ -211,7 +213,9 @@ test.describe("PostCSS", () => { await app.goto("/css-modules-test"); let testId = "css-modules-postcss-context"; let postcssContext = await jsonFromBase64CssContent({ page, testId }); - expect(postcssContext.remix.vanillaExtract).toBe(false); + expect(postcssContext.remix).toEqual({ + vanillaExtract: false, + }); }); let vanillaExtractFixture = () => ({ @@ -258,7 +262,9 @@ test.describe("PostCSS", () => { await app.goto("/vanilla-extract-test"); let testId = "vanilla-extract-postcss-context"; let postcssContext = await jsonFromBase64CssContent({ page, testId }); - expect(postcssContext.remix.vanillaExtract).toBe(true); + expect(postcssContext.remix).toEqual({ + vanillaExtract: true, + }); }); let cssSideEffectImportsFixture = () => ({ @@ -303,7 +309,9 @@ test.describe("PostCSS", () => { await app.goto("/css-side-effect-imports-test"); let testId = "css-side-effect-imports-postcss-context"; let postcssContext = await jsonFromBase64CssContent({ page, testId }); - expect(postcssContext.remix.vanillaExtract).toBe(false); + expect(postcssContext.remix).toEqual({ + vanillaExtract: false, + }); }); let automaticTailwindPluginInsertionFixture = () => ({ From c991ddf505745ec7d8ce2d8d917891a33847c0fe Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Tue, 24 Jan 2023 11:59:33 +1100 Subject: [PATCH 13/17] refactor PostCSS logic --- packages/remix-dev/compiler/utils/postcss.ts | 31 +++++++++++++------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/packages/remix-dev/compiler/utils/postcss.ts b/packages/remix-dev/compiler/utils/postcss.ts index 2775c36d9af..38488c5de48 100644 --- a/packages/remix-dev/compiler/utils/postcss.ts +++ b/packages/remix-dev/compiler/utils/postcss.ts @@ -54,7 +54,9 @@ export async function loadPostcssPlugins({ ); plugins.push(...postcssConfig.plugins); - } catch (err) {} + } catch (err) { + // If they don't have a PostCSS config, just ignore it. + } } if (config.future.unstable_tailwind) { @@ -101,6 +103,10 @@ let tailwindPluginCache = new Map(); async function loadTailwindPlugin( config: RemixConfig ): Promise { + if (!config.future.unstable_tailwind) { + return null; + } + let { rootDirectory } = config; let cacheKey = rootDirectory; let cachedTailwindPlugin = tailwindPluginCache.get(cacheKey); @@ -108,24 +114,29 @@ async function loadTailwindPlugin( return cachedTailwindPlugin; } - let tailwindPlugin: AcceptedPlugin | null = null; + let tailwindPath: string | null = null; try { - // First ensure we have a Tailwind config + // First ensure they have a Tailwind config require.resolve("./tailwind.config", { paths: [rootDirectory] }); // Load Tailwind from the project directory - let tailwindPath = require.resolve("tailwindcss", { + tailwindPath = require.resolve("tailwindcss", { paths: [rootDirectory], }); + } catch (err) { + // If they don't have a Tailwind config or Tailwind installed, just ignore it. + return null; + } - let importedTailwindPlugin = (await import(tailwindPath))?.default; + let importedTailwindPlugin = tailwindPath + ? (await import(tailwindPath))?.default + : null; - // Check that it declares itself as a PostCSS plugin - if (importedTailwindPlugin && importedTailwindPlugin.postcss) { - tailwindPlugin = importedTailwindPlugin; - } - } catch (err) {} + let tailwindPlugin: AcceptedPlugin | null = + importedTailwindPlugin && importedTailwindPlugin.postcss // Check that it declares itself as a PostCSS plugin + ? importedTailwindPlugin + : null; tailwindPluginCache.set(cacheKey, tailwindPlugin); From 10d65c768658ceda2d4a6e664d0ec05288433617 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Tue, 24 Jan 2023 13:37:46 +1100 Subject: [PATCH 14/17] fix formatting --- docs/guides/styling.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/guides/styling.md b/docs/guides/styling.md index 6a998ec1452..2900cad675d 100644 --- a/docs/guides/styling.md +++ b/docs/guides/styling.md @@ -650,7 +650,6 @@ module.exports = (ctx) => { // PostCSS plugins for other styles... }; }; - ``` ### Manual PostCSS Integration From 4c08eba13e10e694b22cda9a8938fce2e9aea894 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Tue, 24 Jan 2023 14:46:16 +1100 Subject: [PATCH 15/17] convert Tailwind path to import specifier --- packages/remix-dev/compiler/utils/postcss.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/remix-dev/compiler/utils/postcss.ts b/packages/remix-dev/compiler/utils/postcss.ts index 38488c5de48..cbc3ae448b9 100644 --- a/packages/remix-dev/compiler/utils/postcss.ts +++ b/packages/remix-dev/compiler/utils/postcss.ts @@ -1,3 +1,4 @@ +import { pathToFileURL } from "url"; import loadConfig from "postcss-load-config"; import type { AcceptedPlugin, Processor } from "postcss"; import postcss from "postcss"; @@ -130,7 +131,7 @@ async function loadTailwindPlugin( } let importedTailwindPlugin = tailwindPath - ? (await import(tailwindPath))?.default + ? (await import(pathToFileURL(tailwindPath).href))?.default : null; let tailwindPlugin: AcceptedPlugin | null = From 19bb1688ea07f404d294c8b0c06ef150449b8344 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Wed, 25 Jan 2023 07:25:33 +1100 Subject: [PATCH 16/17] fix test names --- integration/postcss-test.ts | 2 +- integration/tailwind-test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/integration/postcss-test.ts b/integration/postcss-test.ts index 16daaeeb16a..e0cb1a8a714 100644 --- a/integration/postcss-test.ts +++ b/integration/postcss-test.ts @@ -336,7 +336,7 @@ test.describe("PostCSS", () => { export function Test() { return (
- Automatic Tailwind plugin test + Automatic Tailwind plugin insertion test
); } diff --git a/integration/tailwind-test.ts b/integration/tailwind-test.ts index 5fca3adcc90..29440d0bac6 100644 --- a/integration/tailwind-test.ts +++ b/integration/tailwind-test.ts @@ -74,7 +74,7 @@ test.describe("Tailwind", () => { ...cssModulesFixture(), ...vanillaExtractClassCompositionFixture(), ...vanillaExtractTailwindFunctionsFixture(), - ...cssSideEffectsFixture(), + ...cssSideEffectImportsFixture(), }, }); appFixture = await createAppFixture(fixture); @@ -197,7 +197,7 @@ test.describe("Tailwind", () => { export function Test() { return (
- Vanilla Extract test + Vanilla Extract class composition test
); } @@ -263,7 +263,7 @@ test.describe("Tailwind", () => { expect(padding).toBe(TEST_PADDING_VALUE); }); - let cssSideEffectsFixture = () => ({ + let cssSideEffectImportsFixture = () => ({ "app/routes/css-side-effect-imports-test.jsx": js` import { Test } from "~/test-components/css-side-effect-imports"; From a0dd10e1c56e907f861ce7169d6d3c57f3704cb5 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Wed, 25 Jan 2023 14:33:17 +1100 Subject: [PATCH 17/17] document manual PostCSS Tailwind setup --- docs/guides/styling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/styling.md b/docs/guides/styling.md index 2900cad675d..e6bee0ba653 100644 --- a/docs/guides/styling.md +++ b/docs/guides/styling.md @@ -469,7 +469,7 @@ export const links: LinksFunction = () => [ With this setup in place, you can also use [Tailwind's functions and directives][tailwind-functions-and-directives] anywhere in your CSS. -Note that if you're also using Remix's [built-in PostCSS support](#built-in-postcss-support), the Tailwind PostCSS plugin will be automatically included if it's missing. +Note that if you're also using Remix's [built-in PostCSS support](#built-in-postcss-support), the Tailwind PostCSS plugin will be automatically included if it's missing, but you can also choose to manually include the Tailwind plugin in your PostCSS config instead if you'd prefer. ### Manual Tailwind Integration