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

perf(dev): cache regular stylesheet compilation #6638

Merged
merged 7 commits into from
Jun 21, 2023
5 changes: 5 additions & 0 deletions .changeset/cache-regular-stylesheets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@remix-run/dev": patch
---

Add caching to regular stylesheet compilation
232 changes: 138 additions & 94 deletions packages/remix-dev/compiler/plugins/cssImports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import esbuild from "esbuild";

import invariant from "../../invariant";
import type { Context } from "../context";
import { getCachedPostcssProcessor } from "../utils/postcss";
import {
getPostcssProcessor,
populateDependenciesFromMessages,
} from "../utils/postcss";
import { absoluteCssUrlsPlugin } from "./absoluteCssUrlsPlugin";

const isExtendedLengthPath = /^\\\\\?\\/;
Expand Down Expand Up @@ -42,109 +45,150 @@ export function cssFilePlugin(ctx: Context): esbuild.Plugin {
} = build.initialOptions;

// eslint-disable-next-line prefer-let/prefer-let -- Avoid needing to repeatedly check for null since const can't be reassigned
const postcssProcessor = await getCachedPostcssProcessor(ctx);
const postcssProcessor = await getPostcssProcessor(ctx);

build.onLoad({ filter: /\.css$/ }, async (args) => {
let { metafile, outputFiles, warnings, errors } = await esbuild.build({
absWorkingDir,
assetNames,
chunkNames,
conditions,
define,
external,
format,
mainFields,
nodePaths,
platform,
publicPath,
sourceRoot,
target,
treeShaking,
tsconfig,
minify: ctx.options.mode === "production",
bundle: true,
minifySyntax: true,
metafile: true,
write: false,
sourcemap: Boolean(ctx.options.sourcemap && postcssProcessor), // We only need source maps if we're processing the CSS with PostCSS
splitting: false,
outdir: ctx.config.assetsBuildDirectory,
entryNames: assetNames,
entryPoints: [args.path],
loader: {
...loader,
".css": "css",
},
plugins: [
absoluteCssUrlsPlugin(),
...(postcssProcessor
? [
{
name: "postcss-plugin",
async setup(build) {
build.onLoad(
{ filter: /\.css$/, namespace: "file" },
async (args) => ({
contents: await postcssProcessor({ path: args.path }),
loader: "css",
})
);
},
} satisfies esbuild.Plugin,
]
: []),
],
});
let cacheKey = `css-file:${args.path}`;
let {
cacheValue: { contents, watchFiles, warnings },
} = await ctx.fileWatchCache.getOrSet(cacheKey, async () => {
let fileDependencies = new Set([args.path]);
let globDependencies = new Set<string>();

let { metafile, outputFiles, warnings, errors } = await esbuild.build(
{
absWorkingDir,
assetNames,
chunkNames,
conditions,
define,
external,
format,
mainFields,
nodePaths,
platform,
publicPath,
sourceRoot,
target,
treeShaking,
tsconfig,
minify: ctx.options.mode === "production",
bundle: true,
minifySyntax: true,
metafile: true,
write: false,
sourcemap: Boolean(ctx.options.sourcemap && postcssProcessor), // We only need source maps if we're processing the CSS with PostCSS
splitting: false,
outdir: ctx.config.assetsBuildDirectory,
entryNames: assetNames,
entryPoints: [args.path],
loader: {
...loader,
".css": "css",
},
plugins: [
absoluteCssUrlsPlugin(),
...(postcssProcessor
? [
{
name: "postcss-plugin",
async setup(build) {
build.onLoad(
{ filter: /\.css$/, namespace: "file" },
async (args) => {
let contents = await fse.readFile(
args.path,
"utf-8"
);

let { css, messages } =
await postcssProcessor.process(contents, {
from: args.path,
to: args.path,
map: ctx.options.sourcemap,
});

if (errors && errors.length) {
return { errors };
}

invariant(metafile, "metafile is missing");
let { outputs } = metafile;
let entry = Object.keys(outputs).find((out) => outputs[out].entryPoint);
invariant(entry, "entry point not found");

let normalizedEntry = path.resolve(
ctx.config.rootDirectory,
normalizePathSlashes(entry)
);
let entryFile = outputFiles.find((file) => {
return (
path.resolve(
ctx.config.rootDirectory,
normalizePathSlashes(file.path)
) === normalizedEntry
populateDependenciesFromMessages({
messages,
fileDependencies,
globDependencies,
});

return {
contents: css,
loader: "css",
};
}
);
},
} satisfies esbuild.Plugin,
]
: []),
],
}
);
});

invariant(entryFile, "entry file not found");
if (errors && errors.length) {
throw { errors };
}

let outputFilesWithoutEntry = outputFiles.filter(
(file) => file !== entryFile
);
invariant(metafile, "metafile is missing");
let { outputs } = metafile;
let entry = Object.keys(outputs).find(
(out) => outputs[out].entryPoint
);
invariant(entry, "entry point not found");

let normalizedEntry = path.resolve(
ctx.config.rootDirectory,
normalizePathSlashes(entry)
);
let entryFile = outputFiles.find((file) => {
return (
path.resolve(
ctx.config.rootDirectory,
normalizePathSlashes(file.path)
) === normalizedEntry
);
});

// write all assets
await Promise.all(
outputFilesWithoutEntry.map(({ path: filepath, contents }) =>
fse.outputFile(filepath, contents)
)
);
invariant(entryFile, "entry file not found");

let outputFilesWithoutEntry = outputFiles.filter(
(file) => file !== entryFile
);

// add all css assets to dependencies
for (let { inputs } of Object.values(outputs)) {
for (let input of Object.keys(inputs)) {
let resolvedInput = path.resolve(input);
fileDependencies.add(resolvedInput);
}
}

// write all assets
await Promise.all(
outputFilesWithoutEntry.map(({ path: filepath, contents }) =>
fse.outputFile(filepath, contents)
)
);

return {
cacheValue: {
contents: entryFile.contents,
// add all dependencies to watchFiles
watchFiles: Array.from(fileDependencies),
warnings,
},
fileDependencies,
globDependencies,
};
});

return {
contents: entryFile.contents,
contents,
loader: "file",
// add all css assets to watchFiles
watchFiles: Object.values(outputs).reduce<string[]>(
(arr, { inputs }) => {
let resolvedInputs = Object.keys(inputs).map((input) => {
return path.resolve(input);
});
arr.push(...resolvedInputs);
return arr;
},
[]
),
watchFiles,
warnings,
};
});
Expand Down