Skip to content

Commit

Permalink
add initial CSS Modules integration
Browse files Browse the repository at this point in the history
  • Loading branch information
markdalgleish committed Nov 24, 2022
1 parent 90447b7 commit 8b9d1b8
Show file tree
Hide file tree
Showing 29 changed files with 1,411 additions and 30 deletions.
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -9,6 +9,7 @@
"packages/remix-cloudflare",
"packages/remix-cloudflare-pages",
"packages/remix-cloudflare-workers",
"packages/remix-css-bundle",
"packages/remix-deno",
"packages/remix-dev",
"packages/remix-eslint-config",
Expand Down
13 changes: 13 additions & 0 deletions packages/remix-css-bundle/README.md
@@ -0,0 +1,13 @@
# Welcome to Remix!

[Remix](https://remix.run) is a web framework that helps you build better websites with React.

To get started, open a new shell and run:

```sh
npx create-remix@latest
```

Then follow the prompts you see in your terminal.

For more information about Remix, [visit remix.run](https://remix.run)!
3 changes: 3 additions & 0 deletions packages/remix-css-bundle/browser.ts
@@ -0,0 +1,3 @@
import type { AssetsManifest } from "@remix-run/dev/assets-manifest";
let assetsManifest: AssetsManifest = (window as any).__remixManifest;
export default assetsManifest.cssBundleHref;
23 changes: 23 additions & 0 deletions packages/remix-css-bundle/package.json
@@ -0,0 +1,23 @@
{
"name": "@remix-run/css-bundle",
"description": "Entrypoint for the CSS bundle created by Remix",
"version": "1.7.5",
"license": "MIT",
"main": "./server.js",
"module": "./esm/server.js",
"browser": {
"./server.js": "./browser.js",
"./esm/server.js": "./esm/browser.js"
},
"repository": {
"type": "git",
"url": "https://github.com/remix-run/remix",
"directory": "packages/remix-css-bundle"
},
"bugs": {
"url": "https://github.com/remix-run/remix/issues"
},
"dependencies": {
"@remix-run/dev": "1.7.6"
}
}
69 changes: 69 additions & 0 deletions packages/remix-css-bundle/rollup.config.js
@@ -0,0 +1,69 @@
const babel = require("@rollup/plugin-babel").default;
const nodeResolve = require("@rollup/plugin-node-resolve").default;
const copy = require("rollup-plugin-copy");

const {
copyToPlaygrounds,
createBanner,
getOutputDir,
isBareModuleId,
} = require("../../rollup.utils");
const { name: packageName, version } = require("./package.json");

/** @returns {import("rollup").RollupOptions[]} */
module.exports = function rollup() {
let sourceDir = "packages/remix-css-bundle";
let outputDir = getOutputDir(packageName);

return [
{
external(id) {
return isBareModuleId(id);
},
input: [`${sourceDir}/browser.ts`, `${sourceDir}/server.ts`],
output: {
banner: createBanner(packageName, version),
dir: outputDir,
format: "cjs",
preserveModules: true,
exports: "named",
},
plugins: [
babel({
babelHelpers: "bundled",
exclude: /node_modules/,
extensions: [".ts"],
}),
nodeResolve({ extensions: [".ts"] }),
copy({
targets: [
{ src: `LICENSE.md`, dest: outputDir },
{ src: `${sourceDir}/package.json`, dest: outputDir },
{ src: `${sourceDir}/README.md`, dest: outputDir },
],
}),
],
},
{
external(id) {
return isBareModuleId(id);
},
input: [`${sourceDir}/browser.ts`, `${sourceDir}/server.ts`],
output: {
banner: createBanner(packageName, version),
dir: `${outputDir}/esm`,
format: "esm",
preserveModules: true,
},
plugins: [
babel({
babelHelpers: "bundled",
exclude: /node_modules/,
extensions: [".ts"],
}),
nodeResolve({ extensions: [".ts"] }),
copyToPlaygrounds(),
],
},
];
};
2 changes: 2 additions & 0 deletions packages/remix-css-bundle/server.ts
@@ -0,0 +1,2 @@
import assetsManifest from "@remix-run/dev/assets-manifest";
export default assetsManifest.cssBundleHref;
18 changes: 18 additions & 0 deletions packages/remix-css-bundle/tsconfig.json
@@ -0,0 +1,18 @@
{
"exclude": ["__tests__"],
"compilerOptions": {
"lib": ["ES2019", "DOM.Iterable"],
"target": "ES2019",

"module": "CommonJS",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"strict": true,
"declaration": true,
"emitDeclarationOnly": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"outDir": "../../build/node_modules/@remix-run/css-bundle",
"rootDir": "."
}
}
4 changes: 4 additions & 0 deletions packages/remix-dev/assets-manifest.d.ts
@@ -0,0 +1,4 @@
import type { AssetsManifest } from "@remix-run/dev";
declare const manifest: AssetsManifest;
export type { AssetsManifest };
export default manifest;
29 changes: 24 additions & 5 deletions packages/remix-dev/compiler/assets.ts
Expand Up @@ -31,12 +31,31 @@ export interface AssetsManifest {
hasErrorBoundary: boolean;
};
};
cssBundleHref?: string;
}

export async function createAssetsManifest(
config: RemixConfig,
metafile: esbuild.Metafile
): Promise<AssetsManifest> {
export async function createAssetsManifest({
config,
metafile,
cssMetafile,
}: {
config: RemixConfig;
metafile: esbuild.Metafile;
cssMetafile: esbuild.Metafile;
}): Promise<AssetsManifest> {
let cssBundlePathPrefix = path.join(
config.relativeAssetsBuildDirectory,
"css-bundle"
);

let cssBundleHref = Object.keys(cssMetafile.outputs).find(
(output) =>
output.startsWith(cssBundlePathPrefix) && output.endsWith(".css")
);
if (cssBundleHref) {
cssBundleHref = resolveUrl(cssBundleHref);
}

function resolveUrl(outputPath: string): string {
return createUrl(
config.publicPath,
Expand Down Expand Up @@ -109,7 +128,7 @@ export async function createAssetsManifest(
optimizeRoutes(routes, entry.imports);
let version = getHash(JSON.stringify({ entry, routes })).slice(0, 8);

return { version, entry, routes };
return { version, entry, routes, cssBundleHref };
}

type ImportsCache = { [routeId: string]: string[] };
Expand Down
83 changes: 59 additions & 24 deletions packages/remix-dev/compiler/compileBrowser.ts
Expand Up @@ -10,12 +10,15 @@ import { getAppDependencies } from "./dependencies";
import { loaders } from "./loaders";
import { type CompileOptions } from "./options";
import { browserRouteModulesPlugin } from "./plugins/browserRouteModulesPlugin";
import { cssModulesPlugin } from "./plugins/cssModulesPlugin";
import { cssFilePlugin } from "./plugins/cssFilePlugin";
import { emptyModulesPlugin } from "./plugins/emptyModulesPlugin";
import { mdxPlugin } from "./plugins/mdx";
import { urlImportsPlugin } from "./plugins/urlImportsPlugin";
import { type WriteChannel } from "./utils/channel";
import { writeFileSafe } from "./utils/fs";
import { cssBuildVirtualModule } from "./virtualModules";
import { cssEntryModulePlugin } from "./plugins/cssEntryModulePlugin";

export type BrowserCompiler = {
// produce ./public/build/
Expand Down Expand Up @@ -57,20 +60,31 @@ const writeAssetsManifest = async (
};

const createEsbuildConfig = (
build: "app" | "css",
config: RemixConfig,
options: CompileOptions
): esbuild.BuildOptions | esbuild.BuildIncremental => {
let entryPoints: esbuild.BuildOptions["entryPoints"] = {
"entry.client": path.resolve(config.appDirectory, config.entryClientFile),
};
for (let id of Object.keys(config.routes)) {
// All route entry points are virtual modules that will be loaded by the
// browserEntryPointsPlugin. This allows us to tree-shake server-only code
// that we don't want to run in the browser (i.e. action & loader).
entryPoints[id] = config.routes[id].file + "?browser";
let entryPoints: esbuild.BuildOptions["entryPoints"] = {};
if (build === "css") {
entryPoints = {
"css-bundle": cssBuildVirtualModule.id,
};
} else {
entryPoints = {
"entry.client": path.resolve(config.appDirectory, config.entryClientFile),
};

for (let id of Object.keys(config.routes)) {
// All route entry points are virtual modules that will be loaded by the
// browserEntryPointsPlugin. This allows us to tree-shake server-only code
// that we don't want to run in the browser (i.e. action & loader).
entryPoints[id] = config.routes[id].file + "?browser";
}
}

let plugins = [
cssModulesPlugin(options),
cssEntryModulePlugin(config),
cssFilePlugin(options),
urlImportsPlugin(),
mdxPlugin(config),
Expand All @@ -89,7 +103,7 @@ const createEsbuildConfig = (
loader: loaders,
bundle: true,
logLevel: "silent",
splitting: true,
splitting: build !== "css",
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
Expand Down Expand Up @@ -118,26 +132,47 @@ export const createBrowserCompiler = (
remixConfig: RemixConfig,
options: CompileOptions
): BrowserCompiler => {
let compiler: esbuild.BuildIncremental;
let esbuildConfig = createEsbuildConfig(remixConfig, options);
let appCompiler: esbuild.BuildIncremental;
let cssCompiler: esbuild.BuildIncremental;

let appEsbuildConfig = createEsbuildConfig("app", remixConfig, options);
let cssEsbuildConfig = createEsbuildConfig("css", remixConfig, options);

let compile = async (manifestChannel: WriteChannel<AssetsManifest>) => {
let metafile: esbuild.Metafile;
if (compiler === undefined) {
compiler = await esbuild.build({
...esbuildConfig,
metafile: true,
incremental: true,
});
metafile = compiler.metafile!;
} else {
metafile = (await compiler.rebuild()).metafile!;
}
let manifest = await createAssetsManifest(remixConfig, metafile);
let appBuildResult = !appCompiler
? esbuild.build({
...appEsbuildConfig,
metafile: true,
incremental: true,
})
: appCompiler.rebuild();

let cssBuildResult = !cssCompiler
? esbuild.build({
...cssEsbuildConfig,
metafile: true,
incremental: true,
})
: cssCompiler.rebuild();

[appCompiler, cssCompiler] = await Promise.all([
appBuildResult,
cssBuildResult,
]);

let manifest = await createAssetsManifest({
config: remixConfig,
metafile: appCompiler.metafile!,
cssMetafile: cssCompiler.metafile!,
});
manifestChannel.write(manifest);
await writeAssetsManifest(remixConfig, manifest);
};
return {
compile,
dispose: () => compiler?.rebuild.dispose(),
dispose: () => {
appCompiler?.rebuild.dispose();
cssCompiler?.rebuild.dispose();
},
};
};
2 changes: 2 additions & 0 deletions packages/remix-dev/compiler/compilerServer.ts
Expand Up @@ -8,6 +8,7 @@ import { type RemixConfig } from "../config";
import { type AssetsManifest } from "./assets";
import { loaders } from "./loaders";
import { type CompileOptions } from "./options";
import { cssModulesPlugin } from "./plugins/cssModulesPlugin";
import { cssFilePlugin } from "./plugins/cssFilePlugin";
import { emptyModulesPlugin } from "./plugins/emptyModulesPlugin";
import { mdxPlugin } from "./plugins/mdx";
Expand Down Expand Up @@ -48,6 +49,7 @@ const createEsbuildConfig = (
let isDenoRuntime = config.serverBuildTarget === "deno";

let plugins: esbuild.Plugin[] = [
cssModulesPlugin(options),
cssFilePlugin(options),
urlImportsPlugin(),
mdxPlugin(config),
Expand Down
43 changes: 43 additions & 0 deletions packages/remix-dev/compiler/plugins/cssEntryModulePlugin.ts
@@ -0,0 +1,43 @@
import type { Plugin } from "esbuild";

import type { RemixConfig } from "../../config";
import { cssBuildVirtualModule } from "../virtualModules";

/**
* Creates a virtual module called `@remix-run/dev/css-build` that imports all
* browser build entry points so that any reachable CSS can be included in a
* single file at the end of the build.
*/
export function cssEntryModulePlugin(config: RemixConfig): Plugin {
let filter = cssBuildVirtualModule.filter;

return {
name: "css-entry-module",
setup(build) {
build.onResolve({ filter }, ({ path }) => {
return {
path,
namespace: "css-entry-module",
};
});

build.onLoad({ filter }, async () => {
return {
resolveDir: config.appDirectory,
loader: "js",
contents: [
`export * as entryClient from ${JSON.stringify(
`./${config.entryClientFile}`
)};`,
...Object.keys(config.routes).map((key, index) => {
let route = config.routes[key];
return `export * as route${index} from ${JSON.stringify(
`./${route.file}`
)};`;
}),
].join("\n"),
};
});
},
};
}
13 changes: 13 additions & 0 deletions packages/remix-dev/compiler/plugins/cssModulesPlugin.ts
@@ -0,0 +1,13 @@
import { type CompileOptions } from "../options";
import esbuildCssModulesPlugin from "./esbuild-plugin-css-modules/index.js";

export function cssModulesPlugin({ mode }: { mode: CompileOptions["mode"] }) {
return esbuildCssModulesPlugin({
inject: false,
filter: /\.module\.css$/i, // The default includes support for "*.modules.css", so we're limiting the scope here
v2: true,
v2CssModulesOption: {
pattern: mode === "production" ? "[hash]" : "[name]_[local]_[hash]",
},
});
}

0 comments on commit 8b9d1b8

Please sign in to comment.