From 04c0a2f6cfb4873473819839f0992de71f455bef Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 23 Feb 2022 12:14:13 -0800 Subject: [PATCH 01/71] Initial implementation of CSS modules plugins for both server and browser --- .../remix-dev/compiler/plugins/cssModules.ts | 184 ++++++++++++++++++ packages/remix-dev/compiler/routes.ts | 3 + packages/remix-dev/package.json | 2 + yarn.lock | 160 ++++++++++++--- 4 files changed, 318 insertions(+), 31 deletions(-) create mode 100644 packages/remix-dev/compiler/plugins/cssModules.ts diff --git a/packages/remix-dev/compiler/plugins/cssModules.ts b/packages/remix-dev/compiler/plugins/cssModules.ts new file mode 100644 index 00000000000..1f30ac49660 --- /dev/null +++ b/packages/remix-dev/compiler/plugins/cssModules.ts @@ -0,0 +1,184 @@ +import postcss from "postcss"; +import type { Result as PostCSSResult } from "postcss"; +import cssModules from "postcss-modules"; +import path from "path"; +import * as fse from "fs-extra"; +import type * as esbuild from "esbuild"; + +import { getFileHash } from "../utils/crypto"; +import * as cache from "../../cache"; +import type { RemixConfig } from "../../config"; + +type CSSModuleClassMap = { [key: string]: string }; + +const suffixMatcher = /\.module\.css?$/; + +/** + * Loads *.module.css files on the server build and returns the hashed JSON so + * we can get the right classnames in the HTML. + */ +export function serverCssModulesPlugin(config: RemixConfig): esbuild.Plugin { + return { + name: "server-css-modules", + async setup(build) { + build.onResolve({ filter: suffixMatcher }, (args) => { + return { + path: getResolvedFilePath(config, args), + namespace: "server-css-modules", + }; + }); + + build.onLoad({ filter: suffixMatcher }, async (args) => { + try { + let { json } = await processCssCached(config, args.path); + + return { + contents: JSON.stringify(json), + loader: "json", + }; + } catch (err: any) { + return { + errors: [{ text: err.message }], + }; + } + }); + }, + }; +} + +/** + * Loads *.module.css files in the browser build and calls back with the + * processed CSS so it can be compiled into a single global file. + */ +export function browserCssModulesPlugin( + config: RemixConfig, + handleProcessedCss: (css: string) => void +): esbuild.Plugin { + return { + name: "browser-css-modules", + async setup(build) { + build.onResolve({ filter: suffixMatcher }, (args) => { + return { + path: getResolvedFilePath(config, args), + namespace: "browser-css-modules", + // It's safe to remove this import if the classnames aren't used anywhere. + sideEffects: false, + }; + }); + + build.onLoad({ filter: suffixMatcher }, async (args) => { + try { + let { css, json } = await processCssCached(config, args.path); + + handleProcessedCss(css); + + return { + contents: JSON.stringify(json), + loader: "json", + }; + } catch (err: any) { + return { + errors: [{ text: err.message }], + }; + } + }); + }, + }; +} + +interface ProcessedCSS { + css: string; + json: CSSModuleClassMap; +} + +let memoryCssCache = new Map< + string, + { hash: string; processedCssPromise: Promise } +>(); + +async function processCssCached( + config: RemixConfig, + filePath: string +): Promise { + let file = path.resolve(config.appDirectory, filePath); + let hash = await getFileHash(file); + + // Use an in-memory cache to prevent browser + server builds from compiling + // the same CSS at the same time. They can re-use each other's work! + let cached = memoryCssCache.get(file); + if (cached) { + if (cached.hash === hash) { + return cached.processedCssPromise; + } else { + // Contents of the file changed, get it out of the in-memory cache. + memoryCssCache.delete(file); + } + } + + // Use an on-disk cache to speed up dev server boot. + let processedCssPromise = (async function () { + let key = file + ".cssmodule"; + + let cached: (ProcessedCSS & { hash: string }) | null = null; + try { + cached = await cache.getJson(config.cacheDirectory, key); + } catch (error) { + // Ignore cache read errors. + } + + if (!cached || cached.hash !== hash) { + let { css, json } = await processCss(filePath); + + cached = { hash, css, json }; + + try { + await cache.putJson(config.cacheDirectory, key, cached); + } catch (error) { + // Ignore cache put errors. + } + } + + return { + css: cached.css, + json: cached.json, + }; + })(); + + memoryCssCache.set(file, { hash, processedCssPromise }); + + return processedCssPromise; +} + +async function processCss(file: string) { + let json: CSSModuleClassMap = {}; + + let source = await fse.readFile(file, "utf-8"); + let result = await postcss([ + cssModules({ + localsConvention: "camelCase", + generateScopedName: "[name]__[local]___[hash:base64:8]", + hashPrefix: "remix", + getJSON(_, data) { + json = { ...data }; + return json; + }, + }), + ]).process(source, { + from: undefined, + map: false, + }); + + // TODO: Support sourcemaps when using .module.css files + return { css: result.css, json }; +} + +function getResolvedFilePath( + config: RemixConfig, + args: { path: string; resolveDir: string } +) { + // TODO: Ideally we should deal with the "~/" higher up in the build process + // if possible. + return args.path.startsWith("~/") + ? path.resolve(config.appDirectory, args.path.replace(/^~\//, "")) + : path.resolve(args.resolveDir, args.path); +} diff --git a/packages/remix-dev/compiler/routes.ts b/packages/remix-dev/compiler/routes.ts index 06b9f36a595..30d72354ab4 100644 --- a/packages/remix-dev/compiler/routes.ts +++ b/packages/remix-dev/compiler/routes.ts @@ -33,6 +33,9 @@ export async function getRouteModuleExportsCached( } } + // TODO: Why is this here? The only purpose of this function is to cache the + // result of getRouteModuleExports! This should either be there or somewhere + // in the caller. // Layout routes can't have actions if (routeId.match(/\/__[\s\w\d_-]+$/) && cached.exports.includes("action")) { throw new Error(`Actions are not supported in layout routes: ${routeId}`); diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 98cfde52f7c..7935f6bcb28 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -26,6 +26,8 @@ "lodash.debounce": "^4.0.8", "meow": "^7.1.1", "minimatch": "^3.0.4", + "postcss": "^8.4.6", + "postcss-modules": "^4.3.1", "pretty-ms": "^7.0.1", "read-package-json-fast": "^2.0.2", "remark-frontmatter": "^4.0.0", diff --git a/yarn.lock b/yarn.lock index b881b44928a..bba7836684f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,14 +9,13 @@ dependencies: "@jridgewell/trace-mapping" "^0.3.0" -"@architect/functions@^4.1.1": - version "4.1.1" - resolved "https://registry.yarnpkg.com/@architect/functions/-/functions-4.1.1.tgz#4520f6070858f8731c1da403e40591f3909fcfec" - integrity sha512-x0+B/V9Jo5onksOce6iYdYeLh5F91dOp5fTnGJdkgOFCS+N0YN9zlI2asdPIs1Rb6xxCwoH1nuzs/jnNkupQWA== +"@architect/functions@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@architect/functions/-/functions-5.0.2.tgz#748dcb71376cc0794bfd7ad4dcf85ab6f825cd74" + integrity sha512-bph1tyJpKTdKuyX32imWKy2hbHOzKGs9aHS6UhZYNirj9cuX/O0RWgHks/PKaJ/Hx4qewFaCmF2UL08knWpSeA== dependencies: - aws-serverless-express "^3.4.0" - cookie "^0.4.1" - cookie-signature "^1.1.0" + cookie "^0.4.2" + cookie-signature "^1.2.0" csrf "^3.1.0" node-webtokens "^1.0.4" run-parallel "^1.2.0" @@ -2567,14 +2566,6 @@ "@typescript-eslint/types" "5.12.1" eslint-visitor-keys "^3.0.0" -"@vendia/serverless-express@^3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@vendia/serverless-express/-/serverless-express-3.4.0.tgz#156f47d364b067ae6fa678a914c51887f494321a" - integrity sha512-/UAAbi9qRjUtjRISt5MJ1sfhtrHb26hqQ0nvE5qhMLsAdR5H7ErBwPD8Q/v2OENKm0iWsGwErIZEg7ebUeFDjQ== - dependencies: - binary-case "^1.0.0" - type-is "^1.6.16" - "@vercel/node@^1.8.3": version "1.12.1" resolved "https://registry.npmjs.org/@vercel/node/-/node-1.12.1.tgz" @@ -3148,15 +3139,6 @@ aws-sdk@^2.820.0: uuid "3.3.2" xml2js "0.4.19" -aws-serverless-express@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/aws-serverless-express/-/aws-serverless-express-3.4.0.tgz#74153b8cc80dbd2c6a32a51e6d353a325c2710d7" - integrity sha512-YG9ZjAOI9OpwqDDWzkRc3kKJYJuR7gTMjLa3kAWopO17myoprxskCUyCEee+RKe34tcR4UNrVtgAwW5yDe74bw== - dependencies: - "@vendia/serverless-express" "^3.4.0" - binary-case "^1.0.0" - type-is "^1.6.16" - axe-core@^4.3.5: version "4.3.5" resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.3.5.tgz" @@ -3304,11 +3286,6 @@ big.js@^5.2.2: resolved "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== -binary-case@^1.0.0: - version "1.1.4" - resolved "https://registry.yarnpkg.com/binary-case/-/binary-case-1.1.4.tgz#d687104d59e38f2b9e658d3a58936963c59ab931" - integrity sha512-9Kq8m6NZTAgy05Ryuh7U3Qc4/ujLQU1AZ5vMw4cr3igTdi5itZC6kCNrRr2X8NzPiDn2oUIFTfa71DKMnue/Zg== - binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" @@ -3980,6 +3957,11 @@ cookie-signature@^1.1.0: resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.1.0.tgz" integrity sha512-Alvs19Vgq07eunykd3Xy2jF0/qSNv2u7KDbAek9H5liV1UMijbqFs5cycZvv5dVsvseT/U4H8/7/w8Koh35C4A== +cookie-signature@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.0.tgz#4deed303f5f095e7a02c979e3fcb19157f5eaeea" + integrity sha512-R0BOPfLGTitaKhgKROKZQN6iyq2iDQcH1DOF8nJoaWapguX5bC2w+Q/I9NmmM5lfcvEarnLZr+cCvmEYYSXvYA== + cookie@0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz" @@ -3990,6 +3972,11 @@ cookie@^0.4.1: resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz" integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== +cookie@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + cookiejar@^2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz" @@ -4072,6 +4059,11 @@ css@^3.0.0: source-map "^0.6.1" source-map-resolve "^0.6.0" +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + cssom@^0.4.4: version "0.4.4" resolved "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz" @@ -5427,6 +5419,13 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +generic-names@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/generic-names/-/generic-names-4.0.0.tgz#0bd8a2fd23fe8ea16cbd0a279acd69c06933d9a3" + integrity sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A== + dependencies: + loader-utils "^3.2.0" + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" @@ -5794,6 +5793,16 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +icss-replace-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" + integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0= + +icss-utils@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" + integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== + ieee754@1.1.13: version "1.1.13" resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz" @@ -7129,6 +7138,11 @@ loader-utils@^2.0.0: emojis-list "^3.0.0" json5 "^2.1.2" +loader-utils@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.2.0.tgz#bcecc51a7898bee7473d4bc6b845b23af8304d4f" + integrity sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ== + locate-path@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz" @@ -7144,6 +7158,11 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= + lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" @@ -7980,6 +7999,11 @@ nanocolors@^0.1.0, nanocolors@^0.1.5: resolved "https://registry.npmjs.org/nanocolors/-/nanocolors-0.1.12.tgz" integrity sha512-2nMHqg1x5PU+unxX7PGY7AuYxl2qDx7PSrTRjizr8sxdd3l/3hBuWWaki62qmtYm2U5i4Z5E7GbjlyDFhs9/EQ== +nanoid@^3.2.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" + integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -8521,6 +8545,70 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= +postcss-modules-extract-imports@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" + integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== + +postcss-modules-local-by-default@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c" + integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ== + dependencies: + icss-utils "^5.0.0" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.1.0" + +postcss-modules-scope@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06" + integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== + dependencies: + postcss-selector-parser "^6.0.4" + +postcss-modules-values@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" + integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== + dependencies: + icss-utils "^5.0.0" + +postcss-modules@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/postcss-modules/-/postcss-modules-4.3.1.tgz#517c06c09eab07d133ae0effca2c510abba18048" + integrity sha512-ItUhSUxBBdNamkT3KzIZwYNNRFKmkJrofvC2nWab3CPKhYBQ1f27XXh1PAPE27Psx58jeelPsxWB/+og+KEH0Q== + dependencies: + generic-names "^4.0.0" + icss-replace-symbols "^1.1.0" + lodash.camelcase "^4.3.0" + postcss-modules-extract-imports "^3.0.0" + postcss-modules-local-by-default "^4.0.0" + postcss-modules-scope "^3.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: + version "6.0.9" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.9.tgz#ee71c3b9ff63d9cd130838876c13a2ec1a992b2f" + integrity sha512-UO3SgnZOVTwu4kyLR22UQ1xZh086RyNZppb7lLAKBFK8a32ttG5i87Y/P3+2bRSjZNyJ1B7hfFNo273tKe9YxQ== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-value-parser@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss@^8.4.6: + version "8.4.6" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.6.tgz#c5ff3c3c457a23864f32cb45ac9b741498a09ae1" + integrity sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA== + dependencies: + nanoid "^3.2.0" + picocolors "^1.0.0" + source-map-js "^1.0.2" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" @@ -9553,6 +9641,11 @@ sort-package-json@^1.54.0: is-plain-obj "2.1.0" sort-object-keys "^1.1.3" +source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + source-map-resolve@^0.5.0: version "0.5.3" resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" @@ -9693,6 +9786,11 @@ streamsearch@0.1.2: resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz" integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= +string-hash@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" + integrity sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs= + string-length@^4.0.1: version "4.0.2" resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz" @@ -10206,7 +10304,7 @@ type-fest@^2.11.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.11.2.tgz#5534a919858bc517492cd3a53a673835a76d2e71" integrity sha512-reW2Y2Mpn0QNA/5fvtm5doROLwDPu2zOm5RtY7xQQS05Q7xgC8MOZ3yPNaP9m/s/sNjjFQtHo7VCNqYW2iI+Ig== -type-is@^1.6.16, type-is@^1.6.18, type-is@~1.6.17, type-is@~1.6.18: +type-is@^1.6.18, type-is@~1.6.17, type-is@~1.6.18: version "1.6.18" resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== @@ -10483,7 +10581,7 @@ use@^3.1.0: resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== -util-deprecate@^1.0.1: +util-deprecate@^1.0.1, util-deprecate@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= From 8b76cccc8f8b0f6aea4d788d7d16093ede3fed50 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Thu, 24 Feb 2022 06:24:19 -0800 Subject: [PATCH 02/71] chore: use correct promise resolve type for AssetsManifestRef --- .../remix-dev/compiler/plugins/serverAssetsManifestPlugin.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/remix-dev/compiler/plugins/serverAssetsManifestPlugin.ts b/packages/remix-dev/compiler/plugins/serverAssetsManifestPlugin.ts index fd434042ccc..561de097b6f 100644 --- a/packages/remix-dev/compiler/plugins/serverAssetsManifestPlugin.ts +++ b/packages/remix-dev/compiler/plugins/serverAssetsManifestPlugin.ts @@ -3,8 +3,9 @@ import jsesc from "jsesc"; import invariant from "../../invariant"; import { assetsManifestVirtualModule } from "../virtualModules"; +import type { AssetsManifest } from "../../compiler/assets"; -export type AssetsManifestPromiseRef = { current?: Promise }; +export type AssetsManifestPromiseRef = { current?: Promise }; /** * Creates a virtual module called `@remix-run/dev/assets-manifest` that exports From 75c61ab29c894ec0f0cb5dc8dc5d301e0814bf32 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Thu, 24 Feb 2022 07:32:33 -0800 Subject: [PATCH 03/71] Add virtual modules plugins for CSS Modules --- .../remix-dev/compiler/plugins/cssModules.ts | 72 +++++++++++++++++-- packages/remix-dev/compiler/virtualModules.ts | 5 ++ 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/packages/remix-dev/compiler/plugins/cssModules.ts b/packages/remix-dev/compiler/plugins/cssModules.ts index 1f30ac49660..90466fabf04 100644 --- a/packages/remix-dev/compiler/plugins/cssModules.ts +++ b/packages/remix-dev/compiler/plugins/cssModules.ts @@ -1,5 +1,4 @@ import postcss from "postcss"; -import type { Result as PostCSSResult } from "postcss"; import cssModules from "postcss-modules"; import path from "path"; import * as fse from "fs-extra"; @@ -8,8 +7,17 @@ import type * as esbuild from "esbuild"; import { getFileHash } from "../utils/crypto"; import * as cache from "../../cache"; import type { RemixConfig } from "../../config"; +import { cssModulesVirtualModule } from "../virtualModules"; +import type { AssetsManifestPromiseRef } from "./serverAssetsManifestPlugin"; -type CSSModuleClassMap = { [key: string]: string }; +export interface CssModulesRef { + current: { + filePath?: string | undefined; + content: string; + }; +} + +type CSSModuleClassMap = Record; const suffixMatcher = /\.module\.css?$/; @@ -153,13 +161,14 @@ async function processCss(file: string) { let json: CSSModuleClassMap = {}; let source = await fse.readFile(file, "utf-8"); - let result = await postcss([ + + let { css } = await postcss([ cssModules({ localsConvention: "camelCase", generateScopedName: "[name]__[local]___[hash:base64:8]", hashPrefix: "remix", getJSON(_, data) { - json = { ...data }; + json = { ...json, ...data }; return json; }, }), @@ -169,7 +178,7 @@ async function processCss(file: string) { }); // TODO: Support sourcemaps when using .module.css files - return { css: result.css, json }; + return { css, json }; } function getResolvedFilePath( @@ -182,3 +191,56 @@ function getResolvedFilePath( ? path.resolve(config.appDirectory, args.path.replace(/^~\//, "")) : path.resolve(args.resolveDir, args.path); } + +/** + * Creates a virtual module called `@remix-run/dev/css-modules` that exports the + * URL of the compiled CSS that users will use in their route's `link` export. + */ +export function serverCssModulesModulePlugin( + assetsManifestPromiseRef: AssetsManifestPromiseRef +): esbuild.Plugin { + let filter = cssModulesVirtualModule.filter; + return { + name: "css-modules-module", + setup(build) { + build.onResolve({ filter }, async () => { + let filePath = (await assetsManifestPromiseRef.current)?.cssModules; + return { + path: filePath, + namespace: "server-css-modules-module", + }; + }); + + build.onLoad({ filter }, async (args) => { + return { + resolveDir: args.path, + loader: "css", + }; + }); + }, + }; +} + +export function browserCssModulesModulePlugin( + cssModulesFilePath: string | undefined +): esbuild.Plugin { + let filter = cssModulesVirtualModule.filter; + return { + name: "css-modules-module", + setup(build) { + build.onResolve({ filter }, async () => { + return { + path: cssModulesFilePath, + namespace: "browser-css-modules-module", + }; + }); + + build.onLoad({ filter }, async (args) => { + return { + resolveDir: args.path, + loader: "css", + }; + }); + }, + }; +} diff --git a/packages/remix-dev/compiler/virtualModules.ts b/packages/remix-dev/compiler/virtualModules.ts index e7df45fbd92..3601c3f7740 100644 --- a/packages/remix-dev/compiler/virtualModules.ts +++ b/packages/remix-dev/compiler/virtualModules.ts @@ -12,3 +12,8 @@ export const assetsManifestVirtualModule: VirtualModule = { id: "@remix-run/dev/assets-manifest", filter: /^@remix-run\/dev\/assets-manifest$/, }; + +export const cssModulesVirtualModule: VirtualModule = { + id: "@remix-run/dev/css-modules", + filter: /^@remix-run\/dev\/css-modules$/, +}; From 42eb07f0235c860859e1e828a43b0b7740ae09fc Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Thu, 24 Feb 2022 09:33:33 -0800 Subject: [PATCH 04/71] add cssmodules file path to assets object --- packages/remix-dev/compiler/assets.ts | 11 +++++++++-- packages/remix-dev/compiler/plugins/cssModules.ts | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/remix-dev/compiler/assets.ts b/packages/remix-dev/compiler/assets.ts index 4656c8328ee..2c910a5ec7a 100644 --- a/packages/remix-dev/compiler/assets.ts +++ b/packages/remix-dev/compiler/assets.ts @@ -31,11 +31,13 @@ export interface AssetsManifest { hasErrorBoundary: boolean; }; }; + cssModules: string | undefined; } export async function createAssetsManifest( config: RemixConfig, - metafile: esbuild.Metafile + metafile: esbuild.Metafile, + cssModulesPath: string | undefined ): Promise { function resolveUrl(outputPath: string): string { return createUrl( @@ -106,7 +108,12 @@ 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, + cssModules: cssModulesPath ? resolveUrl(cssModulesPath) : undefined, + }; } type ImportsCache = { [routeId: string]: string[] }; diff --git a/packages/remix-dev/compiler/plugins/cssModules.ts b/packages/remix-dev/compiler/plugins/cssModules.ts index 90466fabf04..6e0ba10e01f 100644 --- a/packages/remix-dev/compiler/plugins/cssModules.ts +++ b/packages/remix-dev/compiler/plugins/cssModules.ts @@ -168,7 +168,7 @@ async function processCss(file: string) { generateScopedName: "[name]__[local]___[hash:base64:8]", hashPrefix: "remix", getJSON(_, data) { - json = { ...json, ...data }; + json = { ...data }; return json; }, }), From ea79fd50719c19cc8c2e73a4b7ffafbd4a914846 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Thu, 24 Feb 2022 09:39:19 -0800 Subject: [PATCH 05/71] modify scope + comments --- packages/remix-dev/compiler/plugins/cssModules.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/remix-dev/compiler/plugins/cssModules.ts b/packages/remix-dev/compiler/plugins/cssModules.ts index 6e0ba10e01f..802f5b4dbdf 100644 --- a/packages/remix-dev/compiler/plugins/cssModules.ts +++ b/packages/remix-dev/compiler/plugins/cssModules.ts @@ -165,7 +165,12 @@ async function processCss(file: string) { let { css } = await postcss([ cssModules({ localsConvention: "camelCase", - generateScopedName: "[name]__[local]___[hash:base64:8]", + // [name] -> CSS modules file-name (button.module.css -> button-module) + // [local] -> locally assigned classname + // example: + // in button.module.css: .button {} + // generated classname: .button-module__button_wtIDeq {} + generateScopedName: "[name]__[local]_[hash:base64:8]", hashPrefix: "remix", getJSON(_, data) { json = { ...data }; From 3af5759dfe8c76f3182965ec29c31a6f60bb859c Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Thu, 24 Feb 2022 16:41:34 -0800 Subject: [PATCH 06/71] refactor CSS modules plugins --- packages/remix-dev/compiler.ts | 261 ++++++++++++++++-- packages/remix-dev/compiler/assets.ts | 9 +- .../remix-dev/compiler/plugins/cssModules.ts | 206 +++++++------- 3 files changed, 335 insertions(+), 141 deletions(-) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index def1c9c7f90..142e39ced70 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -22,6 +22,16 @@ import { serverAssetsManifestPlugin } from "./compiler/plugins/serverAssetsManif import { serverBareModulesPlugin } from "./compiler/plugins/serverBareModulesPlugin"; import { serverEntryModulePlugin } from "./compiler/plugins/serverEntryModulePlugin"; import { serverRouteModulesPlugin } from "./compiler/plugins/serverRouteModulesPlugin"; +import type { + CssModuleClassMap, + CssModulesResults, + CssModulesBuildPromiseRef, +} from "./compiler/plugins/cssModules"; +import { + cssModulesPlugin, + cssModulesFakerPlugin, + getCssModulesFilePath, +} from "./compiler/plugins/cssModules"; import { writeFileSafe } from "./compiler/utils/fs"; // When we build Remix, this shim file is copied directly into the output @@ -77,14 +87,20 @@ export async function build( }: BuildOptions = {} ): Promise { let assetsManifestPromiseRef: AssetsManifestPromiseRef = {}; + let cssModulesBuildPromiseRef: CssModulesBuildPromiseRef = {}; - await buildEverything(config, assetsManifestPromiseRef, { - mode, - target, - sourcemap, - onWarning, - onBuildFailure, - }); + await buildEverything( + config, + assetsManifestPromiseRef, + cssModulesBuildPromiseRef, + { + mode, + target, + sourcemap, + onWarning, + onBuildFailure, + } + ); } interface WatchOptions extends BuildOptions { @@ -122,20 +138,25 @@ export async function watch( }; let assetsManifestPromiseRef: AssetsManifestPromiseRef = {}; - let [browserBuild, serverBuild] = await buildEverything( + let cssModulesBuildPromiseRef: CssModulesBuildPromiseRef = {}; + let [cssModulesBuild, browserBuild, serverBuild] = await buildEverything( config, assetsManifestPromiseRef, + cssModulesBuildPromiseRef, options ); - let initialBuildComplete = !!browserBuild && !!serverBuild; + let initialBuildComplete = + !!cssModulesBuild && !!browserBuild && !!serverBuild; if (initialBuildComplete) { onInitialBuild?.(); } function disposeBuilders() { + cssModulesBuild?.rebuild?.dispose(); browserBuild?.rebuild?.dispose(); serverBuild?.rebuild?.dispose(); + cssModulesBuild = undefined; browserBuild = undefined; serverBuild = undefined; } @@ -154,28 +175,35 @@ export async function watch( let builders = await buildEverything( config, assetsManifestPromiseRef, + cssModulesBuildPromiseRef, options ); if (onRebuildFinish) onRebuildFinish(); - browserBuild = builders[0]; - serverBuild = builders[1]; + + [cssModulesBuild, browserBuild, serverBuild] = builders; }, 500); let rebuildEverything = debounce(async () => { if (onRebuildStart) onRebuildStart(); - if (!browserBuild?.rebuild || !serverBuild?.rebuild) { + if ( + !cssModulesBuild?.rebuild || + !browserBuild?.rebuild || + !serverBuild?.rebuild + ) { disposeBuilders(); try { - [browserBuild, serverBuild] = await buildEverything( + [cssModulesBuild, browserBuild, serverBuild] = await buildEverything( config, assetsManifestPromiseRef, + cssModulesBuildPromiseRef, options ); if (!initialBuildComplete) { - initialBuildComplete = !!browserBuild && !!serverBuild; + initialBuildComplete = + !!cssModulesBuild && !!browserBuild && !!serverBuild; if (initialBuildComplete) { onInitialBuild?.(); } @@ -189,16 +217,19 @@ export async function watch( // If we get here and can't call rebuild something went wrong and we // should probably blow as it's not really recoverable. + let cssModulesBuildPromise = cssModulesBuild.rebuild(); let browserBuildPromise = browserBuild.rebuild(); let assetsManifestPromise = browserBuildPromise.then((build) => - generateAssetsManifest(config, build.metafile!) + generateAssetsManifest(config, build.metafile!, cssModulesBuildPromiseRef) ); // Assign the assetsManifestPromise to a ref so the server build can await // it when loading the @remix-run/dev/assets-manifest virtual module. assetsManifestPromiseRef.current = assetsManifestPromise; + cssModulesBuildPromiseRef.current = cssModulesBuildPromise; await Promise.all([ + cssModulesBuildPromise, assetsManifestPromise, serverBuild .rebuild() @@ -278,39 +309,79 @@ function isEntryPoint(config: RemixConfig, file: string) { /////////////////////////////////////////////////////////////////////////////// +interface BrowserBuild extends esbuild.BuildResult {} +interface ServerBuild extends esbuild.BuildResult {} + +interface BuildInvalidate { + (): Promise; + dispose(): void; +} +export interface CssModulesBuild extends CssModulesResults { + result: esbuild.BuildResult; + rebuild?: BuildInvalidate; +} + +interface CssModulesBuildIncremental extends CssModulesBuild { + rebuild: BuildInvalidate; +} + async function buildEverything( config: RemixConfig, assetsManifestPromiseRef: AssetsManifestPromiseRef, + cssModulesBuildPromiseRef: CssModulesBuildPromiseRef, options: Required & { incremental?: boolean } -): Promise<(esbuild.BuildResult | undefined)[]> { +): Promise< + [ + CssModulesBuild | undefined, + BrowserBuild | undefined, + ServerBuild | undefined + ] +> { try { - let browserBuildPromise = createBrowserBuild(config, options); + let cssModulesPromise = createCssModulesBuild( + config, + options, + assetsManifestPromiseRef + ); + + let browserBuildPromise = createBrowserBuild( + config, + cssModulesBuildPromiseRef, + options + ); let assetsManifestPromise = browserBuildPromise.then((build) => - generateAssetsManifest(config, build.metafile!) + generateAssetsManifest(config, build.metafile!, cssModulesBuildPromiseRef) ); // Assign the assetsManifestPromise to a ref so the server build can await // it when loading the @remix-run/dev/assets-manifest virtual module. assetsManifestPromiseRef.current = assetsManifestPromise; + // Assign the cssModulesBuildPromiseRef to a ref so both server and client builds + // can await it when loading the @remix-run/dev/css-modules virtual module. + cssModulesBuildPromiseRef.current = cssModulesPromise; + let serverBuildPromise = createServerBuild( config, - options, - assetsManifestPromiseRef + assetsManifestPromiseRef, + cssModulesBuildPromiseRef, + options ); return await Promise.all([ + cssModulesPromise, assetsManifestPromise.then(() => browserBuildPromise), serverBuildPromise, ]); } catch (err) { options.onBuildFailure(err as Error); - return [undefined, undefined]; + return [undefined, undefined, undefined]; } } async function createBrowserBuild( config: RemixConfig, + cssModulesBuildPromiseRef: CssModulesBuildPromiseRef, options: BuildOptions & { incremental?: boolean } ): Promise { // For the browser build, exclude node built-ins that don't have a @@ -369,6 +440,7 @@ async function createBrowserBuild( ), }, plugins: [ + cssModulesFakerPlugin(config, cssModulesBuildPromiseRef), mdxPlugin(config), browserRouteModulesPlugin(config, /\?browser$/), emptyModulesPlugin(config, /\.server(\.[jt]sx?)?$/), @@ -379,8 +451,9 @@ async function createBrowserBuild( async function createServerBuild( config: RemixConfig, - options: Required & { incremental?: boolean }, - assetsManifestPromiseRef: AssetsManifestPromiseRef + assetsManifestPromiseRef: AssetsManifestPromiseRef, + cssModulesBuildPromiseRef: CssModulesBuildPromiseRef, + options: Required & { incremental?: boolean } ): Promise { let dependencies = await getAppDependencies(config); @@ -398,6 +471,7 @@ async function createServerBuild( } let plugins: esbuild.Plugin[] = [ + cssModulesFakerPlugin(config, cssModulesBuildPromiseRef), mdxPlugin(config), emptyModulesPlugin(config, /\.client(\.[jt]sx?)?$/), serverRouteModulesPlugin(config), @@ -455,11 +529,148 @@ async function createServerBuild( }); } +async function createCssModulesBuild( + config: RemixConfig, + options: Required & { incremental?: boolean }, + assetsManifestPromiseRef: AssetsManifestPromiseRef +): Promise { + // ... find all .module.css files + // aggregate the contents, keep an object of the JSON + // run the full build (we don't care about JS modules...) + // output only a single file to public/build + // return result to browser/server builds + + let cssModulesContent = ""; + let cssModulesJson: CssModuleClassMap = {}; + let cssModulesMap: Record = + {}; + function handleProcessedCss( + filePath: string, + css: string, + json: CssModuleClassMap + ) { + cssModulesContent += css; + cssModulesJson = { ...cssModulesJson, ...json }; + cssModulesMap = { + ...cssModulesMap, + [filePath]: { + css, + json, + }, + }; + } + + // The rest of this is copied from the server build + + let dependencies = await getAppDependencies(config); + + let stdin: esbuild.StdinOptions | undefined; + let entryPoints: string[] | undefined; + + if (config.serverEntryPoint) { + entryPoints = [config.serverEntryPoint]; + } else { + stdin = { + contents: config.serverBuildTargetEntryModule, + resolveDir: config.rootDirectory, + loader: "ts", + }; + } + + let plugins: esbuild.Plugin[] = [ + mdxPlugin(config), + cssModulesPlugin(config, handleProcessedCss), + emptyModulesPlugin(config, /\.client(\.[jt]sx?)?$/), + serverRouteModulesPlugin(config), + serverEntryModulePlugin(config), + serverAssetsManifestPlugin(assetsManifestPromiseRef), + serverBareModulesPlugin(config, dependencies), + ]; + + if (config.serverPlatform !== "node") { + plugins.unshift(NodeModulesPolyfillPlugin()); + } + + return esbuild + .build({ + absWorkingDir: config.rootDirectory, + stdin, + entryPoints, + outfile: config.serverBuildPath, + write: false, + platform: config.serverPlatform, + format: config.serverModuleFormat, + treeShaking: true, + minify: + options.mode === BuildMode.Production && + !!config.serverBuildTarget && + ["cloudflare-workers", "cloudflare-pages"].includes( + config.serverBuildTarget + ), + mainFields: + config.serverModuleFormat === "esm" + ? ["module", "main"] + : ["main", "module"], + target: options.target, + inject: [reactShim], + loader: loaders, + bundle: true, + logLevel: "silent", + incremental: options.incremental, + sourcemap: options.sourcemap ? "inline" : false, + // The server build needs to know how to generate asset URLs for imports + // of CSS and other files. + assetNames: "_assets/[name]-[hash]", + publicPath: config.publicPath, + define: { + "process.env.NODE_ENV": JSON.stringify(options.mode), + "process.env.REMIX_DEV_SERVER_WS_PORT": JSON.stringify( + config.devServerPort + ), + }, + plugins, + }) + .then(async (build) => { + return { + result: build, + rebuild: (() => { + if (!options.incremental) { + return undefined; + } + let builder = (async () => { + // Clear CSS modules data before rebuild + cssModulesContent = ""; + cssModulesMap = {}; + let result = await build.rebuild!(); + return { + result, + rebuild: builder, + filePath: await getCssModulesFilePath(config, cssModulesContent), + moduleMap: cssModulesMap, + }; + }) as BuildInvalidate; + // TODO: unsure about this, check back w/ esbuild docs to clarify what + // dispose is doing and see if we need to clear any internal state + builder.dispose = build.rebuild!.dispose; + return builder; + })(), + filePath: await getCssModulesFilePath(config, cssModulesContent), + moduleMap: cssModulesMap, + }; + }); +} + async function generateAssetsManifest( config: RemixConfig, - metafile: esbuild.Metafile + metafile: esbuild.Metafile, + cssModulesBuildPromiseRef: CssModulesBuildPromiseRef ): Promise { - let assetsManifest = await createAssetsManifest(config, metafile); + let cssModulesResults = await cssModulesBuildPromiseRef.current; + let assetsManifest = await createAssetsManifest( + config, + metafile, + cssModulesResults + ); let filename = `manifest-${assetsManifest.version.toUpperCase()}.js`; assetsManifest.url = config.publicPath + filename; diff --git a/packages/remix-dev/compiler/assets.ts b/packages/remix-dev/compiler/assets.ts index 2c910a5ec7a..61399310348 100644 --- a/packages/remix-dev/compiler/assets.ts +++ b/packages/remix-dev/compiler/assets.ts @@ -6,6 +6,7 @@ import invariant from "../invariant"; import { getRouteModuleExportsCached } from "./routes"; import { getHash } from "./utils/crypto"; import { createUrl } from "./utils/url"; +import type { CssModulesResults } from "./plugins/cssModules"; type Route = RemixConfig["routes"][string]; @@ -31,13 +32,13 @@ export interface AssetsManifest { hasErrorBoundary: boolean; }; }; - cssModules: string | undefined; + cssModules: (CssModulesResults & { fileUrl: string }) | undefined; } export async function createAssetsManifest( config: RemixConfig, metafile: esbuild.Metafile, - cssModulesPath: string | undefined + cssModules: CssModulesResults | undefined ): Promise { function resolveUrl(outputPath: string): string { return createUrl( @@ -112,7 +113,9 @@ export async function createAssetsManifest( version, entry, routes, - cssModules: cssModulesPath ? resolveUrl(cssModulesPath) : undefined, + cssModules: cssModules + ? { ...cssModules, fileUrl: resolveUrl(cssModules.filePath) } + : undefined, }; } diff --git a/packages/remix-dev/compiler/plugins/cssModules.ts b/packages/remix-dev/compiler/plugins/cssModules.ts index 802f5b4dbdf..6cbf131facc 100644 --- a/packages/remix-dev/compiler/plugins/cssModules.ts +++ b/packages/remix-dev/compiler/plugins/cssModules.ts @@ -9,37 +9,37 @@ import * as cache from "../../cache"; import type { RemixConfig } from "../../config"; import { cssModulesVirtualModule } from "../virtualModules"; import type { AssetsManifestPromiseRef } from "./serverAssetsManifestPlugin"; - -export interface CssModulesRef { - current: { - filePath?: string | undefined; - content: string; - }; -} - -type CSSModuleClassMap = Record; +import type { CssModulesBuild } from "../../compiler"; const suffixMatcher = /\.module\.css?$/; /** - * Loads *.module.css files on the server build and returns the hashed JSON so - * we can get the right classnames in the HTML. + * Loads *.module.css files and returns the hashed JSON so we can get the right + * classnames in the HTML. */ -export function serverCssModulesPlugin(config: RemixConfig): esbuild.Plugin { +export function cssModulesPlugin( + config: RemixConfig, + handleProcessedCss: ( + filePath: string, + css: string, + json: CssModuleClassMap + ) => void +): esbuild.Plugin { return { - name: "server-css-modules", + name: "css-modules", async setup(build) { build.onResolve({ filter: suffixMatcher }, (args) => { return { path: getResolvedFilePath(config, args), - namespace: "server-css-modules", + namespace: "css-modules", + sideEffects: false, }; }); build.onLoad({ filter: suffixMatcher }, async (args) => { try { - let { json } = await processCssCached(config, args.path); - + let { css, json } = await processCssCached(config, args.path); + handleProcessedCss(args.path, css, json); return { contents: JSON.stringify(json), loader: "json", @@ -55,79 +55,90 @@ export function serverCssModulesPlugin(config: RemixConfig): esbuild.Plugin { } /** - * Loads *.module.css files in the browser build and calls back with the - * processed CSS so it can be compiled into a single global file. + * This plugin is for the browser + server builds. It doesn't actually process + * the CSS, but it resolves the import paths and yields to the CSS Modules build + * for the results. */ -export function browserCssModulesPlugin( +export function cssModulesFakerPlugin( config: RemixConfig, - handleProcessedCss: (css: string) => void + cssModulesBuildPromiseRef: CssModulesBuildPromiseRef ): esbuild.Plugin { return { - name: "browser-css-modules", + name: "css-modules-faker", async setup(build) { build.onResolve({ filter: suffixMatcher }, (args) => { return { path: getResolvedFilePath(config, args), - namespace: "browser-css-modules", - // It's safe to remove this import if the classnames aren't used anywhere. + namespace: "css-modules", sideEffects: false, }; }); build.onLoad({ filter: suffixMatcher }, async (args) => { - try { - let { css, json } = await processCssCached(config, args.path); - - handleProcessedCss(css); - - return { - contents: JSON.stringify(json), - loader: "json", - }; - } catch (err: any) { - return { - errors: [{ text: err.message }], - }; - } + let res = await cssModulesBuildPromiseRef.current!; + let { json } = res.moduleMap[args.path]; + return { + contents: JSON.stringify(json), + loader: "json", + }; }); }, }; } -interface ProcessedCSS { - css: string; - json: CSSModuleClassMap; -} +/** + * Creates a virtual module called `@remix-run/dev/css-modules` that exports the + * URL of the compiled CSS that users will use in their route's `link` export. + */ +export function cssModulesVirtualModulePlugin( + assetsManifestPromiseRef: AssetsManifestPromiseRef +): esbuild.Plugin { + let filter = cssModulesVirtualModule.filter; + return { + name: "css-modules-virtual-module", + setup(build) { + build.onResolve({ filter }, async ({ path }) => { + return { + path, + namespace: "css-modules-virtual-module", + }; + }); -let memoryCssCache = new Map< - string, - { hash: string; processedCssPromise: Promise } ->(); + build.onLoad({ filter }, async () => { + let fileUrl = (await assetsManifestPromiseRef.current)?.cssModules + ?.fileUrl; + + return { + loader: "js", + contents: `export default ${ + fileUrl + ? `"${fileUrl}"` + : // If there is no CSS module file the import should return undefined. + // Users should check for a value and conditionally render a + // link only if we have modules. This way we can avoid annoying + // errors if they delete any references to CSS modules, though + // perhaps a dev warning would be helpful if they intended to + // remove CSS modules but left the virtual module import! + "undefined" + }`, + }; + }); + }, + }; +} async function processCssCached( config: RemixConfig, filePath: string -): Promise { +): Promise { let file = path.resolve(config.appDirectory, filePath); let hash = await getFileHash(file); - // Use an in-memory cache to prevent browser + server builds from compiling - // the same CSS at the same time. They can re-use each other's work! - let cached = memoryCssCache.get(file); - if (cached) { - if (cached.hash === hash) { - return cached.processedCssPromise; - } else { - // Contents of the file changed, get it out of the in-memory cache. - memoryCssCache.delete(file); - } - } - // Use an on-disk cache to speed up dev server boot. let processedCssPromise = (async function () { let key = file + ".cssmodule"; - let cached: (ProcessedCSS & { hash: string }) | null = null; + let cached: (CssModuleFileContents & { hash: string }) | null = null; try { cached = await cache.getJson(config.cacheDirectory, key); } catch (error) { @@ -136,7 +147,6 @@ async function processCssCached( if (!cached || cached.hash !== hash) { let { css, json } = await processCss(filePath); - cached = { hash, css, json }; try { @@ -152,16 +162,12 @@ async function processCssCached( }; })(); - memoryCssCache.set(file, { hash, processedCssPromise }); - return processedCssPromise; } async function processCss(file: string) { - let json: CSSModuleClassMap = {}; - + let json: CssModuleClassMap = {}; let source = await fse.readFile(file, "utf-8"); - let { css } = await postcss([ cssModules({ localsConvention: "camelCase", @@ -191,61 +197,35 @@ function getResolvedFilePath( args: { path: string; resolveDir: string } ) { // TODO: Ideally we should deal with the "~/" higher up in the build process - // if possible. + // if possible. Also ... what if the user changes this alias in their + // tsconfig? Do we support that? return args.path.startsWith("~/") ? path.resolve(config.appDirectory, args.path.replace(/^~\//, "")) : path.resolve(args.resolveDir, args.path); } -/** - * Creates a virtual module called `@remix-run/dev/css-modules` that exports the - * URL of the compiled CSS that users will use in their route's `link` export. - */ -export function serverCssModulesModulePlugin( - assetsManifestPromiseRef: AssetsManifestPromiseRef -): esbuild.Plugin { - let filter = cssModulesVirtualModule.filter; - return { - name: "css-modules-module", - setup(build) { - build.onResolve({ filter }, async () => { - let filePath = (await assetsManifestPromiseRef.current)?.cssModules; - return { - path: filePath, - namespace: "server-css-modules-module", - }; - }); +export async function getCssModulesFilePath(config: RemixConfig, css: string) { + let hash = (await getFileHash(css)).slice(0, 8).toUpperCase(); + return path.relative( + config.assetsBuildDirectory, + path.resolve(`__css-modules-${hash}.css`) + ); +} - build.onLoad({ filter }, async (args) => { - return { - resolveDir: args.path, - loader: "css", - }; - }); - }, - }; +export interface CssModulesBuildPromiseRef { + current?: Promise; } -export function browserCssModulesModulePlugin( - cssModulesFilePath: string | undefined -): esbuild.Plugin { - let filter = cssModulesVirtualModule.filter; - return { - name: "css-modules-module", - setup(build) { - build.onResolve({ filter }, async () => { - return { - path: cssModulesFilePath, - namespace: "browser-css-modules-module", - }; - }); +export interface CssModuleFileContents { + css: string; + json: CssModuleClassMap; +} - build.onLoad({ filter }, async (args) => { - return { - resolveDir: args.path, - loader: "css", - }; - }); - }, - }; +export type CssModuleFileMap = Record; + +export interface CssModulesResults { + filePath: string; + moduleMap: CssModuleFileMap; } + +export type CssModuleClassMap = Record; From 5726ca3b63dd23e6921ad79fa67397e5b386f59c Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 25 Feb 2022 11:39:32 -0800 Subject: [PATCH 07/71] fix css hashing --- packages/remix-dev/compiler/plugins/cssModules.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/remix-dev/compiler/plugins/cssModules.ts b/packages/remix-dev/compiler/plugins/cssModules.ts index 6cbf131facc..7c8546d55d5 100644 --- a/packages/remix-dev/compiler/plugins/cssModules.ts +++ b/packages/remix-dev/compiler/plugins/cssModules.ts @@ -4,7 +4,7 @@ import path from "path"; import * as fse from "fs-extra"; import type * as esbuild from "esbuild"; -import { getFileHash } from "../utils/crypto"; +import { getFileHash, getHash } from "../utils/crypto"; import * as cache from "../../cache"; import type { RemixConfig } from "../../config"; import { cssModulesVirtualModule } from "../virtualModules"; @@ -204,9 +204,9 @@ function getResolvedFilePath( : path.resolve(args.resolveDir, args.path); } -export async function getCssModulesFilePath(config: RemixConfig, css: string) { - let hash = (await getFileHash(css)).slice(0, 8).toUpperCase(); - return path.relative( +export function getCssModulesFilePath(config: RemixConfig, css: string) { + let hash = getHash(css).slice(0, 8).toUpperCase(); + return path.resolve( config.assetsBuildDirectory, path.resolve(`__css-modules-${hash}.css`) ); From fa30edc2eee921ff483eda5050b2eac907d69428 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Mon, 28 Feb 2022 17:22:34 -0800 Subject: [PATCH 08/71] chore: update assets manifest types --- packages/remix-dev/compiler/assets.ts | 21 ++++++--------------- packages/remix-dev/compiler/utils/url.ts | 9 +++++++++ packages/remix-react/entry.ts | 17 +++++++++++++++++ packages/remix-server-runtime/entry.ts | 17 +++++++++++++++++ 4 files changed, 49 insertions(+), 15 deletions(-) diff --git a/packages/remix-dev/compiler/assets.ts b/packages/remix-dev/compiler/assets.ts index 61399310348..ca0b59095c8 100644 --- a/packages/remix-dev/compiler/assets.ts +++ b/packages/remix-dev/compiler/assets.ts @@ -5,7 +5,7 @@ import type { RemixConfig } from "../config"; import invariant from "../invariant"; import { getRouteModuleExportsCached } from "./routes"; import { getHash } from "./utils/crypto"; -import { createUrl } from "./utils/url"; +import { resolveUrl } from "./utils/url"; import type { CssModulesResults } from "./plugins/cssModules"; type Route = RemixConfig["routes"][string]; @@ -32,7 +32,7 @@ export interface AssetsManifest { hasErrorBoundary: boolean; }; }; - cssModules: (CssModulesResults & { fileUrl: string }) | undefined; + cssModules: CssModulesResults | undefined; } export async function createAssetsManifest( @@ -40,19 +40,12 @@ export async function createAssetsManifest( metafile: esbuild.Metafile, cssModules: CssModulesResults | undefined ): Promise { - function resolveUrl(outputPath: string): string { - return createUrl( - config.publicPath, - path.relative(config.assetsBuildDirectory, path.resolve(outputPath)) - ); - } - function resolveImports( imports: esbuild.Metafile["outputs"][string]["imports"] ): string[] { return imports .filter((im) => im.kind === "import-statement") - .map((im) => resolveUrl(im.path)); + .map((im) => resolveUrl(config, im.path)); } let entryClientFile = path.resolve( @@ -80,7 +73,7 @@ export async function createAssetsManifest( ); if (entryPointFile === entryClientFile) { entry = { - module: resolveUrl(key), + module: resolveUrl(config, key), imports: resolveImports(output.imports), }; // Only parse routes otherwise dynamic imports can fall into here and fail the build @@ -94,7 +87,7 @@ export async function createAssetsManifest( path: route.path, index: route.index, caseSensitive: route.caseSensitive, - module: resolveUrl(key), + module: resolveUrl(config, key), imports: resolveImports(output.imports), hasAction: sourceExports.includes("action"), hasLoader: sourceExports.includes("loader"), @@ -113,9 +106,7 @@ export async function createAssetsManifest( version, entry, routes, - cssModules: cssModules - ? { ...cssModules, fileUrl: resolveUrl(cssModules.filePath) } - : undefined, + cssModules, }; } diff --git a/packages/remix-dev/compiler/utils/url.ts b/packages/remix-dev/compiler/utils/url.ts index 7c30e4d248d..f873af4b2c6 100644 --- a/packages/remix-dev/compiler/utils/url.ts +++ b/packages/remix-dev/compiler/utils/url.ts @@ -1,5 +1,14 @@ import * as path from "path"; +import type { RemixConfig } from "../../config"; + export function createUrl(publicPath: string, file: string): string { return publicPath + file.split(path.win32.sep).join("/"); } + +export function resolveUrl(config: RemixConfig, outputPath: string): string { + return createUrl( + config.publicPath, + path.relative(config.assetsBuildDirectory, path.resolve(outputPath)) + ); +} diff --git a/packages/remix-react/entry.ts b/packages/remix-react/entry.ts index 77feb17a8b7..eacd5fc80d0 100644 --- a/packages/remix-react/entry.ts +++ b/packages/remix-react/entry.ts @@ -22,4 +22,21 @@ export interface AssetsManifest { routes: RouteManifest; url: string; version: string; + cssModules: CssModulesResults | undefined; } + +// TODO: This should ideally be in one place; duped for now from remix-dev +interface CssModulesResults { + filePath: string; + fileUrl: string; + moduleMap: CssModuleFileMap; +} + +interface CssModuleFileContents { + css: string; + json: CssModuleClassMap; +} + +type CssModuleFileMap = Record; + +type CssModuleClassMap = Record; diff --git a/packages/remix-server-runtime/entry.ts b/packages/remix-server-runtime/entry.ts index 8a555c429a4..1ce158aa282 100644 --- a/packages/remix-server-runtime/entry.ts +++ b/packages/remix-server-runtime/entry.ts @@ -27,8 +27,25 @@ export interface AssetsManifest { routes: RouteManifest; url: string; version: string; + cssModules: CssModulesResults | undefined; } +// TODO: This should ideally be in one place; duped for now from remix-dev +interface CssModulesResults { + filePath: string; + fileUrl: string; + moduleMap: CssModuleFileMap; +} + +interface CssModuleFileContents { + css: string; + json: CssModuleClassMap; +} + +type CssModuleFileMap = Record; + +type CssModuleClassMap = Record; + export function createEntryMatches( matches: RouteMatch[], routes: RouteManifest From aec002360800bcf71b1f8026420d703ebc56e6e2 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Mon, 28 Feb 2022 17:23:07 -0800 Subject: [PATCH 09/71] chore: compiler fixes --- packages/remix-dev/compiler.ts | 140 ++++++++++-------- .../remix-dev/compiler/plugins/cssModules.ts | 51 +++++-- packages/remix-dev/compiler/virtualModules.ts | 4 +- packages/remix-dev/modules.ts | 8 + 4 files changed, 129 insertions(+), 74 deletions(-) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index 142e39ced70..6b72736321d 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -24,13 +24,15 @@ import { serverEntryModulePlugin } from "./compiler/plugins/serverEntryModulePlu import { serverRouteModulesPlugin } from "./compiler/plugins/serverRouteModulesPlugin"; import type { CssModuleClassMap, - CssModulesResults, CssModulesBuildPromiseRef, + CssModulesResults, } from "./compiler/plugins/cssModules"; import { cssModulesPlugin, cssModulesFakerPlugin, - getCssModulesFilePath, + cssModulesVirtualModulePlugin, + cssModulesVirtualModuleFakerPlugin, + getCssModulesFileReferences, } from "./compiler/plugins/cssModules"; import { writeFileSafe } from "./compiler/utils/fs"; @@ -218,6 +220,8 @@ export async function watch( // If we get here and can't call rebuild something went wrong and we // should probably blow as it's not really recoverable. let cssModulesBuildPromise = cssModulesBuild.rebuild(); + cssModulesBuildPromiseRef.current = cssModulesBuildPromise; + let browserBuildPromise = browserBuild.rebuild(); let assetsManifestPromise = browserBuildPromise.then((build) => generateAssetsManifest(config, build.metafile!, cssModulesBuildPromiseRef) @@ -226,7 +230,6 @@ export async function watch( // Assign the assetsManifestPromise to a ref so the server build can await // it when loading the @remix-run/dev/assets-manifest virtual module. assetsManifestPromiseRef.current = assetsManifestPromise; - cssModulesBuildPromiseRef.current = cssModulesBuildPromise; await Promise.all([ cssModulesBuildPromise, @@ -338,17 +341,18 @@ async function buildEverything( ] > { try { - let cssModulesPromise = createCssModulesBuild( - config, - options, - assetsManifestPromiseRef - ); + let cssModulesBuildPromise = createCssModulesBuild(config, options); + + // Assign the cssModulesBuildPromiseRef to a ref so both server and client + // builds can await it when loading the virtual module. + cssModulesBuildPromiseRef.current = cssModulesBuildPromise; let browserBuildPromise = createBrowserBuild( config, cssModulesBuildPromiseRef, options ); + let assetsManifestPromise = browserBuildPromise.then((build) => generateAssetsManifest(config, build.metafile!, cssModulesBuildPromiseRef) ); @@ -357,10 +361,6 @@ async function buildEverything( // it when loading the @remix-run/dev/assets-manifest virtual module. assetsManifestPromiseRef.current = assetsManifestPromise; - // Assign the cssModulesBuildPromiseRef to a ref so both server and client builds - // can await it when loading the @remix-run/dev/css-modules virtual module. - cssModulesBuildPromiseRef.current = cssModulesPromise; - let serverBuildPromise = createServerBuild( config, assetsManifestPromiseRef, @@ -369,7 +369,7 @@ async function buildEverything( ); return await Promise.all([ - cssModulesPromise, + cssModulesBuildPromise, assetsManifestPromise.then(() => browserBuildPromise), serverBuildPromise, ]); @@ -441,6 +441,7 @@ async function createBrowserBuild( }, plugins: [ cssModulesFakerPlugin(config, cssModulesBuildPromiseRef), + cssModulesVirtualModulePlugin(cssModulesBuildPromiseRef), mdxPlugin(config), browserRouteModulesPlugin(config, /\?browser$/), emptyModulesPlugin(config, /\.server(\.[jt]sx?)?$/), @@ -472,6 +473,7 @@ async function createServerBuild( let plugins: esbuild.Plugin[] = [ cssModulesFakerPlugin(config, cssModulesBuildPromiseRef), + cssModulesVirtualModulePlugin(cssModulesBuildPromiseRef), mdxPlugin(config), emptyModulesPlugin(config, /\.client(\.[jt]sx?)?$/), serverRouteModulesPlugin(config), @@ -531,8 +533,7 @@ async function createServerBuild( async function createCssModulesBuild( config: RemixConfig, - options: Required & { incremental?: boolean }, - assetsManifestPromiseRef: AssetsManifestPromiseRef + options: Required & { incremental?: boolean } ): Promise { // ... find all .module.css files // aggregate the contents, keep an object of the JSON @@ -560,66 +561,54 @@ async function createCssModulesBuild( }; } - // The rest of this is copied from the server build + // The rest of this is copied from the browser build let dependencies = await getAppDependencies(config); + let dependencyNames = Object.keys(dependencies); + let externals = nodeBuiltins.filter((mod) => !dependencyNames.includes(mod)); + let fakeBuiltins = nodeBuiltins.filter((mod) => + dependencyNames.includes(mod) + ); - let stdin: esbuild.StdinOptions | undefined; - let entryPoints: string[] | undefined; - - if (config.serverEntryPoint) { - entryPoints = [config.serverEntryPoint]; - } else { - stdin = { - contents: config.serverBuildTargetEntryModule, - resolveDir: config.rootDirectory, - loader: "ts", - }; + if (fakeBuiltins.length > 0) { + throw new Error( + `It appears you're using a module that is built in to node, but you installed it as a dependency which could cause problems. Please remove ${fakeBuiltins.join( + ", " + )} before continuing.` + ); } - let plugins: esbuild.Plugin[] = [ - mdxPlugin(config), - cssModulesPlugin(config, handleProcessedCss), - emptyModulesPlugin(config, /\.client(\.[jt]sx?)?$/), - serverRouteModulesPlugin(config), - serverEntryModulePlugin(config), - serverAssetsManifestPlugin(assetsManifestPromiseRef), - serverBareModulesPlugin(config, dependencies), - ]; - - if (config.serverPlatform !== "node") { - plugins.unshift(NodeModulesPolyfillPlugin()); + 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] = + path.resolve(config.appDirectory, config.routes[id].file) + "?browser"; } return esbuild .build({ - absWorkingDir: config.rootDirectory, - stdin, entryPoints, - outfile: config.serverBuildPath, - write: false, - platform: config.serverPlatform, - format: config.serverModuleFormat, - treeShaking: true, - minify: - options.mode === BuildMode.Production && - !!config.serverBuildTarget && - ["cloudflare-workers", "cloudflare-pages"].includes( - config.serverBuildTarget - ), - mainFields: - config.serverModuleFormat === "esm" - ? ["module", "main"] - : ["main", "module"], - target: options.target, + outdir: config.assetsBuildDirectory, + platform: "browser", + format: "esm", + external: externals, inject: [reactShim], loader: loaders, bundle: true, logLevel: "silent", + splitting: true, + sourcemap: options.sourcemap, + metafile: true, incremental: options.incremental, - sourcemap: options.sourcemap ? "inline" : false, - // The server build needs to know how to generate asset URLs for imports - // of CSS and other files. + mainFields: ["browser", "module", "main"], + treeShaking: true, + minify: options.mode === BuildMode.Production, + entryNames: "[dir]/[name]-[hash]", + chunkNames: "_shared/[name]-[hash]", assetNames: "_assets/[name]-[hash]", publicPath: config.publicPath, define: { @@ -628,9 +617,24 @@ async function createCssModulesBuild( config.devServerPort ), }, - plugins, + plugins: [ + cssModulesPlugin(config, handleProcessedCss), + cssModulesVirtualModuleFakerPlugin(), + mdxPlugin(config), + browserRouteModulesPlugin(config, /\?browser$/), + emptyModulesPlugin(config, /\.server(\.[jt]sx?)?$/), + NodeModulesPolyfillPlugin(), + ], }) .then(async (build) => { + let [filePath, fileUrl] = getCssModulesFileReferences( + config, + cssModulesContent + ); + + await fse.ensureDir(path.dirname(filePath)); + await fse.writeFile(filePath, cssModulesContent); + return { result: build, rebuild: (() => { @@ -642,10 +646,19 @@ async function createCssModulesBuild( cssModulesContent = ""; cssModulesMap = {}; let result = await build.rebuild!(); + let [filePath, fileUrl] = getCssModulesFileReferences( + config, + cssModulesContent + ); + + await fse.ensureDir(path.dirname(filePath)); + await fse.writeFile(filePath, cssModulesContent); + return { result, rebuild: builder, - filePath: await getCssModulesFilePath(config, cssModulesContent), + filePath, + fileUrl, moduleMap: cssModulesMap, }; }) as BuildInvalidate; @@ -654,7 +667,8 @@ async function createCssModulesBuild( builder.dispose = build.rebuild!.dispose; return builder; })(), - filePath: await getCssModulesFilePath(config, cssModulesContent), + filePath, + fileUrl, moduleMap: cssModulesMap, }; }); diff --git a/packages/remix-dev/compiler/plugins/cssModules.ts b/packages/remix-dev/compiler/plugins/cssModules.ts index 7c8546d55d5..ae8e410781a 100644 --- a/packages/remix-dev/compiler/plugins/cssModules.ts +++ b/packages/remix-dev/compiler/plugins/cssModules.ts @@ -8,8 +8,8 @@ import { getFileHash, getHash } from "../utils/crypto"; import * as cache from "../../cache"; import type { RemixConfig } from "../../config"; import { cssModulesVirtualModule } from "../virtualModules"; -import type { AssetsManifestPromiseRef } from "./serverAssetsManifestPlugin"; import type { CssModulesBuild } from "../../compiler"; +import { resolveUrl } from "../utils/url"; const suffixMatcher = /\.module\.css?$/; @@ -29,8 +29,9 @@ export function cssModulesPlugin( name: "css-modules", async setup(build) { build.onResolve({ filter: suffixMatcher }, (args) => { + let path = getResolvedFilePath(config, args); return { - path: getResolvedFilePath(config, args), + path, namespace: "css-modules", sideEffects: false, }; @@ -87,11 +88,11 @@ export function cssModulesFakerPlugin( } /** - * Creates a virtual module called `@remix-run/dev/css-modules` that exports the + * Creates a virtual module called `@remix-run/dev/modules.css` that exports the * URL of the compiled CSS that users will use in their route's `link` export. */ export function cssModulesVirtualModulePlugin( - assetsManifestPromiseRef: AssetsManifestPromiseRef + cssModulesBuildPromiseRef: CssModulesBuildPromiseRef ): esbuild.Plugin { let filter = cssModulesVirtualModule.filter; return { @@ -105,8 +106,7 @@ export function cssModulesVirtualModulePlugin( }); build.onLoad({ filter }, async () => { - let fileUrl = (await assetsManifestPromiseRef.current)?.cssModules - ?.fileUrl; + let fileUrl = (await cssModulesBuildPromiseRef.current)?.fileUrl; return { loader: "js", @@ -127,6 +127,32 @@ export function cssModulesVirtualModulePlugin( }; } +/** + * We also need a 'faker' plugin for the virtual module since we don't know the + * URL yet. This is basically a noop that just feeds us an empty string since it + * doesn't really matter at this stage on the build. + */ +export function cssModulesVirtualModuleFakerPlugin(): esbuild.Plugin { + let filter = cssModulesVirtualModule.filter; + return { + name: "css-modules-virtual-module-faker", + setup(build) { + build.onResolve({ filter }, async ({ path }) => { + return { + path, + namespace: "css-modules-virtual-module", + }; + }); + build.onLoad({ filter }, async () => { + return { + loader: "js", + contents: `export default "";`, + }; + }); + }, + }; +} + async function processCssCached( config: RemixConfig, filePath: string @@ -204,12 +230,18 @@ function getResolvedFilePath( : path.resolve(args.resolveDir, args.path); } -export function getCssModulesFilePath(config: RemixConfig, css: string) { +export function getCssModulesFileReferences( + config: RemixConfig, + css: string +): [filePath: string, fileUrl: string] { let hash = getHash(css).slice(0, 8).toUpperCase(); - return path.resolve( + let filePath = path.resolve( config.assetsBuildDirectory, - path.resolve(`__css-modules-${hash}.css`) + "_assets", + `__css-modules-${hash}.css` ); + let fileUrl = resolveUrl(config, filePath); + return [filePath, fileUrl]; } export interface CssModulesBuildPromiseRef { @@ -225,6 +257,7 @@ export type CssModuleFileMap = Record; export interface CssModulesResults { filePath: string; + fileUrl: string; moduleMap: CssModuleFileMap; } diff --git a/packages/remix-dev/compiler/virtualModules.ts b/packages/remix-dev/compiler/virtualModules.ts index 3601c3f7740..2b77a8f4d48 100644 --- a/packages/remix-dev/compiler/virtualModules.ts +++ b/packages/remix-dev/compiler/virtualModules.ts @@ -14,6 +14,6 @@ export const assetsManifestVirtualModule: VirtualModule = { }; export const cssModulesVirtualModule: VirtualModule = { - id: "@remix-run/dev/css-modules", - filter: /^@remix-run\/dev\/css-modules$/, + id: "@remix-run/dev/modules.css", + filter: /^@remix-run\/dev\/modules\.css$/, }; diff --git a/packages/remix-dev/modules.ts b/packages/remix-dev/modules.ts index a373d27bb51..5cfff8fbfdb 100644 --- a/packages/remix-dev/modules.ts +++ b/packages/remix-dev/modules.ts @@ -2,6 +2,14 @@ declare module "*.aac" { const asset: string; export default asset; } + +// TODO: This isn't working right now because CSS modules still match `*.css` +// and I haven't yet figured out how to match all *except* for modules. +// See https://github.com/microsoft/TypeScript/issues/38638 +declare module "*.module.css" { + const classes: { readonly [key: string]: string }; + export default classes; +} declare module "*.css" { const asset: string; export default asset; From 3f38369510db126eea99eafd680cb5486602204c Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Mon, 28 Feb 2022 17:23:28 -0800 Subject: [PATCH 10/71] chore: add CSS modules imports to gists app --- fixtures/gists-app/app/components/Counter.js | 8 +++++++- fixtures/gists-app/app/components/Counter.module.css | 3 +++ fixtures/gists-app/app/root.jsx | 4 +++- 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 fixtures/gists-app/app/components/Counter.module.css diff --git a/fixtures/gists-app/app/components/Counter.js b/fixtures/gists-app/app/components/Counter.js index b8c0df44753..bdd3cc4ede3 100644 --- a/fixtures/gists-app/app/components/Counter.js +++ b/fixtures/gists-app/app/components/Counter.js @@ -1,9 +1,15 @@ import { useState } from "react"; +import styles from "./Counter.module.css"; + export default function Counter() { let [count, setCount] = useState(0); return ( - ); diff --git a/fixtures/gists-app/app/components/Counter.module.css b/fixtures/gists-app/app/components/Counter.module.css new file mode 100644 index 00000000000..dc0521e7a93 --- /dev/null +++ b/fixtures/gists-app/app/components/Counter.module.css @@ -0,0 +1,3 @@ +.button { + color: auto; +} diff --git a/fixtures/gists-app/app/root.jsx b/fixtures/gists-app/app/root.jsx index 0bc7438d2da..aee82ecfd20 100644 --- a/fixtures/gists-app/app/root.jsx +++ b/fixtures/gists-app/app/root.jsx @@ -11,6 +11,7 @@ import { useMatches, } from "remix"; import normalizeHref from "@exampledev/new.css/new.css"; +import cssModuleStyles from "@remix-run/dev/modules.css"; import favicon from "../public/favicon.ico"; import stylesHref from "./styles/app.css"; @@ -23,8 +24,9 @@ export function links() { }, { rel: "stylesheet", href: stylesHref }, { rel: "stylesheet", href: "/resources/theme-css" }, + cssModuleStyles != null && { rel: "stylesheet", href: cssModuleStyles }, { rel: "shortcut icon", href: favicon }, - ]; + ].filter(Boolean); } export async function loader({ request }) { From c2e238b333fc3eb3e67910d87800314762240817 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Tue, 1 Mar 2022 14:57:33 -0800 Subject: [PATCH 11/71] remove virtual module stuff, add modules to browser build --- fixtures/gists-app/app/root.jsx | 5 +- packages/remix-dev/compiler.ts | 353 ++++++------------ .../remix-dev/compiler/plugins/cssModules.ts | 78 +--- packages/remix-dev/modules.ts | 18 +- 4 files changed, 133 insertions(+), 321 deletions(-) diff --git a/fixtures/gists-app/app/root.jsx b/fixtures/gists-app/app/root.jsx index aee82ecfd20..031ca3c267a 100644 --- a/fixtures/gists-app/app/root.jsx +++ b/fixtures/gists-app/app/root.jsx @@ -11,7 +11,8 @@ import { useMatches, } from "remix"; import normalizeHref from "@exampledev/new.css/new.css"; -import cssModuleStyles from "@remix-run/dev/modules.css"; +// TODO: This will be a separate module now instead of the virtual module +// import cssModuleStyles from "@remix-run/dev/modules.css"; import favicon from "../public/favicon.ico"; import stylesHref from "./styles/app.css"; @@ -24,7 +25,7 @@ export function links() { }, { rel: "stylesheet", href: stylesHref }, { rel: "stylesheet", href: "/resources/theme-css" }, - cssModuleStyles != null && { rel: "stylesheet", href: cssModuleStyles }, + // cssModuleStyles != null && { rel: "stylesheet", href: cssModuleStyles }, { rel: "shortcut icon", href: favicon }, ].filter(Boolean); } diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index 6b72736321d..9c21d9a5075 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -24,14 +24,11 @@ import { serverEntryModulePlugin } from "./compiler/plugins/serverEntryModulePlu import { serverRouteModulesPlugin } from "./compiler/plugins/serverRouteModulesPlugin"; import type { CssModuleClassMap, - CssModulesBuildPromiseRef, CssModulesResults, } from "./compiler/plugins/cssModules"; import { cssModulesPlugin, cssModulesFakerPlugin, - cssModulesVirtualModulePlugin, - cssModulesVirtualModuleFakerPlugin, getCssModulesFileReferences, } from "./compiler/plugins/cssModules"; import { writeFileSafe } from "./compiler/utils/fs"; @@ -89,20 +86,14 @@ export async function build( }: BuildOptions = {} ): Promise { let assetsManifestPromiseRef: AssetsManifestPromiseRef = {}; - let cssModulesBuildPromiseRef: CssModulesBuildPromiseRef = {}; - await buildEverything( - config, - assetsManifestPromiseRef, - cssModulesBuildPromiseRef, - { - mode, - target, - sourcemap, - onWarning, - onBuildFailure, - } - ); + await buildEverything(config, assetsManifestPromiseRef, { + mode, + target, + sourcemap, + onWarning, + onBuildFailure, + }); } interface WatchOptions extends BuildOptions { @@ -140,25 +131,20 @@ export async function watch( }; let assetsManifestPromiseRef: AssetsManifestPromiseRef = {}; - let cssModulesBuildPromiseRef: CssModulesBuildPromiseRef = {}; - let [cssModulesBuild, browserBuild, serverBuild] = await buildEverything( + let [browserBuild, serverBuild] = await buildEverything( config, assetsManifestPromiseRef, - cssModulesBuildPromiseRef, options ); - let initialBuildComplete = - !!cssModulesBuild && !!browserBuild && !!serverBuild; + let initialBuildComplete = !!browserBuild && !!serverBuild; if (initialBuildComplete) { onInitialBuild?.(); } function disposeBuilders() { - cssModulesBuild?.rebuild?.dispose(); browserBuild?.rebuild?.dispose(); serverBuild?.rebuild?.dispose(); - cssModulesBuild = undefined; browserBuild = undefined; serverBuild = undefined; } @@ -177,35 +163,28 @@ export async function watch( let builders = await buildEverything( config, assetsManifestPromiseRef, - cssModulesBuildPromiseRef, options ); if (onRebuildFinish) onRebuildFinish(); - [cssModulesBuild, browserBuild, serverBuild] = builders; + [browserBuild, serverBuild] = builders; }, 500); let rebuildEverything = debounce(async () => { if (onRebuildStart) onRebuildStart(); - if ( - !cssModulesBuild?.rebuild || - !browserBuild?.rebuild || - !serverBuild?.rebuild - ) { + if (!browserBuild?.rebuild || !serverBuild?.rebuild) { disposeBuilders(); try { - [cssModulesBuild, browserBuild, serverBuild] = await buildEverything( + [browserBuild, serverBuild] = await buildEverything( config, assetsManifestPromiseRef, - cssModulesBuildPromiseRef, options ); if (!initialBuildComplete) { - initialBuildComplete = - !!cssModulesBuild && !!browserBuild && !!serverBuild; + initialBuildComplete = !!browserBuild && !!serverBuild; if (initialBuildComplete) { onInitialBuild?.(); } @@ -219,12 +198,10 @@ export async function watch( // If we get here and can't call rebuild something went wrong and we // should probably blow as it's not really recoverable. - let cssModulesBuildPromise = cssModulesBuild.rebuild(); - cssModulesBuildPromiseRef.current = cssModulesBuildPromise; let browserBuildPromise = browserBuild.rebuild(); let assetsManifestPromise = browserBuildPromise.then((build) => - generateAssetsManifest(config, build.metafile!, cssModulesBuildPromiseRef) + generateAssetsManifest(config, build) ); // Assign the assetsManifestPromise to a ref so the server build can await @@ -232,7 +209,6 @@ export async function watch( assetsManifestPromiseRef.current = assetsManifestPromise; await Promise.all([ - cssModulesBuildPromise, assetsManifestPromise, serverBuild .rebuild() @@ -312,49 +288,31 @@ function isEntryPoint(config: RemixConfig, file: string) { /////////////////////////////////////////////////////////////////////////////// -interface BrowserBuild extends esbuild.BuildResult {} interface ServerBuild extends esbuild.BuildResult {} interface BuildInvalidate { - (): Promise; + (): Promise; dispose(): void; } -export interface CssModulesBuild extends CssModulesResults { +export interface BrowserBuild extends CssModulesResults { result: esbuild.BuildResult; rebuild?: BuildInvalidate; } -interface CssModulesBuildIncremental extends CssModulesBuild { +interface BrowserBuildIncremental extends BrowserBuild { rebuild: BuildInvalidate; } async function buildEverything( config: RemixConfig, assetsManifestPromiseRef: AssetsManifestPromiseRef, - cssModulesBuildPromiseRef: CssModulesBuildPromiseRef, options: Required & { incremental?: boolean } -): Promise< - [ - CssModulesBuild | undefined, - BrowserBuild | undefined, - ServerBuild | undefined - ] -> { +): Promise<[BrowserBuild | undefined, ServerBuild | undefined]> { try { - let cssModulesBuildPromise = createCssModulesBuild(config, options); - - // Assign the cssModulesBuildPromiseRef to a ref so both server and client - // builds can await it when loading the virtual module. - cssModulesBuildPromiseRef.current = cssModulesBuildPromise; - - let browserBuildPromise = createBrowserBuild( - config, - cssModulesBuildPromiseRef, - options - ); + let browserBuildPromise = createBrowserBuild(config, options); let assetsManifestPromise = browserBuildPromise.then((build) => - generateAssetsManifest(config, build.metafile!, cssModulesBuildPromiseRef) + generateAssetsManifest(config, build) ); // Assign the assetsManifestPromise to a ref so the server build can await @@ -364,183 +322,23 @@ async function buildEverything( let serverBuildPromise = createServerBuild( config, assetsManifestPromiseRef, - cssModulesBuildPromiseRef, options ); return await Promise.all([ - cssModulesBuildPromise, assetsManifestPromise.then(() => browserBuildPromise), serverBuildPromise, ]); } catch (err) { options.onBuildFailure(err as Error); - return [undefined, undefined, undefined]; + return [undefined, undefined]; } } async function createBrowserBuild( config: RemixConfig, - cssModulesBuildPromiseRef: CssModulesBuildPromiseRef, options: BuildOptions & { incremental?: boolean } -): Promise { - // For the browser build, exclude node built-ins that don't have a - // browser-safe alternative installed in node_modules. Nothing should - // *actually* be external in the browser build (we want to bundle all deps) so - // this is really just making sure we don't accidentally have any dependencies - // on node built-ins in browser bundles. - let dependencies = Object.keys(await getAppDependencies(config)); - let externals = nodeBuiltins.filter((mod) => !dependencies.includes(mod)); - let fakeBuiltins = nodeBuiltins.filter((mod) => dependencies.includes(mod)); - - if (fakeBuiltins.length > 0) { - throw new Error( - `It appears you're using a module that is built in to node, but you installed it as a dependency which could cause problems. Please remove ${fakeBuiltins.join( - ", " - )} before continuing.` - ); - } - - 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] = - path.resolve(config.appDirectory, config.routes[id].file) + "?browser"; - } - - return esbuild.build({ - entryPoints, - outdir: config.assetsBuildDirectory, - platform: "browser", - format: "esm", - external: externals, - inject: [reactShim], - loader: loaders, - bundle: true, - logLevel: "silent", - splitting: true, - sourcemap: options.sourcemap, - metafile: true, - incremental: options.incremental, - mainFields: ["browser", "module", "main"], - treeShaking: true, - minify: options.mode === BuildMode.Production, - entryNames: "[dir]/[name]-[hash]", - chunkNames: "_shared/[name]-[hash]", - assetNames: "_assets/[name]-[hash]", - publicPath: config.publicPath, - define: { - "process.env.NODE_ENV": JSON.stringify(options.mode), - "process.env.REMIX_DEV_SERVER_WS_PORT": JSON.stringify( - config.devServerPort - ), - }, - plugins: [ - cssModulesFakerPlugin(config, cssModulesBuildPromiseRef), - cssModulesVirtualModulePlugin(cssModulesBuildPromiseRef), - mdxPlugin(config), - browserRouteModulesPlugin(config, /\?browser$/), - emptyModulesPlugin(config, /\.server(\.[jt]sx?)?$/), - NodeModulesPolyfillPlugin(), - ], - }); -} - -async function createServerBuild( - config: RemixConfig, - assetsManifestPromiseRef: AssetsManifestPromiseRef, - cssModulesBuildPromiseRef: CssModulesBuildPromiseRef, - options: Required & { incremental?: boolean } -): Promise { - let dependencies = await getAppDependencies(config); - - let stdin: esbuild.StdinOptions | undefined; - let entryPoints: string[] | undefined; - - if (config.serverEntryPoint) { - entryPoints = [config.serverEntryPoint]; - } else { - stdin = { - contents: config.serverBuildTargetEntryModule, - resolveDir: config.rootDirectory, - loader: "ts", - }; - } - - let plugins: esbuild.Plugin[] = [ - cssModulesFakerPlugin(config, cssModulesBuildPromiseRef), - cssModulesVirtualModulePlugin(cssModulesBuildPromiseRef), - mdxPlugin(config), - emptyModulesPlugin(config, /\.client(\.[jt]sx?)?$/), - serverRouteModulesPlugin(config), - serverEntryModulePlugin(config), - serverAssetsManifestPlugin(assetsManifestPromiseRef), - serverBareModulesPlugin(config, dependencies), - ]; - - if (config.serverPlatform !== "node") { - plugins.unshift(NodeModulesPolyfillPlugin()); - } - - return esbuild - .build({ - absWorkingDir: config.rootDirectory, - stdin, - entryPoints, - outfile: config.serverBuildPath, - write: false, - platform: config.serverPlatform, - format: config.serverModuleFormat, - treeShaking: true, - minify: - options.mode === BuildMode.Production && - !!config.serverBuildTarget && - ["cloudflare-workers", "cloudflare-pages"].includes( - config.serverBuildTarget - ), - mainFields: - config.serverModuleFormat === "esm" - ? ["module", "main"] - : ["main", "module"], - target: options.target, - inject: [reactShim], - loader: loaders, - bundle: true, - logLevel: "silent", - incremental: options.incremental, - sourcemap: options.sourcemap ? "inline" : false, - // The server build needs to know how to generate asset URLs for imports - // of CSS and other files. - assetNames: "_assets/[name]-[hash]", - publicPath: config.publicPath, - define: { - "process.env.NODE_ENV": JSON.stringify(options.mode), - "process.env.REMIX_DEV_SERVER_WS_PORT": JSON.stringify( - config.devServerPort - ), - }, - plugins, - }) - .then(async (build) => { - await writeServerBuildResult(config, build.outputFiles); - return build; - }); -} - -async function createCssModulesBuild( - config: RemixConfig, - options: Required & { incremental?: boolean } -): Promise { - // ... find all .module.css files - // aggregate the contents, keep an object of the JSON - // run the full build (we don't care about JS modules...) - // output only a single file to public/build - // return result to browser/server builds - +): Promise { let cssModulesContent = ""; let cssModulesJson: CssModuleClassMap = {}; let cssModulesMap: Record = @@ -561,14 +359,14 @@ async function createCssModulesBuild( }; } - // The rest of this is copied from the browser build - - let dependencies = await getAppDependencies(config); - let dependencyNames = Object.keys(dependencies); - let externals = nodeBuiltins.filter((mod) => !dependencyNames.includes(mod)); - let fakeBuiltins = nodeBuiltins.filter((mod) => - dependencyNames.includes(mod) - ); + // For the browser build, exclude node built-ins that don't have a + // browser-safe alternative installed in node_modules. Nothing should + // *actually* be external in the browser build (we want to bundle all deps) so + // this is really just making sure we don't accidentally have any dependencies + // on node built-ins in browser bundles. + let dependencies = Object.keys(await getAppDependencies(config)); + let externals = nodeBuiltins.filter((mod) => !dependencies.includes(mod)); + let fakeBuiltins = nodeBuiltins.filter((mod) => dependencies.includes(mod)); if (fakeBuiltins.length > 0) { throw new Error( @@ -619,7 +417,6 @@ async function createCssModulesBuild( }, plugins: [ cssModulesPlugin(config, handleProcessedCss), - cssModulesVirtualModuleFakerPlugin(), mdxPlugin(config), browserRouteModulesPlugin(config, /\?browser$/), emptyModulesPlugin(config, /\.server(\.[jt]sx?)?$/), @@ -674,15 +471,97 @@ async function createCssModulesBuild( }); } +async function createServerBuild( + config: RemixConfig, + assetsManifestPromiseRef: AssetsManifestPromiseRef, + options: Required & { incremental?: boolean } +): Promise { + let dependencies = await getAppDependencies(config); + + let stdin: esbuild.StdinOptions | undefined; + let entryPoints: string[] | undefined; + + if (config.serverEntryPoint) { + entryPoints = [config.serverEntryPoint]; + } else { + stdin = { + contents: config.serverBuildTargetEntryModule, + resolveDir: config.rootDirectory, + loader: "ts", + }; + } + + let plugins: esbuild.Plugin[] = [ + cssModulesFakerPlugin(config, assetsManifestPromiseRef), + mdxPlugin(config), + emptyModulesPlugin(config, /\.client(\.[jt]sx?)?$/), + serverRouteModulesPlugin(config), + serverEntryModulePlugin(config), + serverAssetsManifestPlugin(assetsManifestPromiseRef), + serverBareModulesPlugin(config, dependencies), + ]; + + if (config.serverPlatform !== "node") { + plugins.unshift(NodeModulesPolyfillPlugin()); + } + + return esbuild + .build({ + absWorkingDir: config.rootDirectory, + stdin, + entryPoints, + outfile: config.serverBuildPath, + write: false, + platform: config.serverPlatform, + format: config.serverModuleFormat, + treeShaking: true, + minify: + options.mode === BuildMode.Production && + !!config.serverBuildTarget && + ["cloudflare-workers", "cloudflare-pages"].includes( + config.serverBuildTarget + ), + mainFields: + config.serverModuleFormat === "esm" + ? ["module", "main"] + : ["main", "module"], + target: options.target, + inject: [reactShim], + loader: loaders, + bundle: true, + logLevel: "silent", + incremental: options.incremental, + sourcemap: options.sourcemap ? "inline" : false, + // The server build needs to know how to generate asset URLs for imports + // of CSS and other files. + assetNames: "_assets/[name]-[hash]", + publicPath: config.publicPath, + define: { + "process.env.NODE_ENV": JSON.stringify(options.mode), + "process.env.REMIX_DEV_SERVER_WS_PORT": JSON.stringify( + config.devServerPort + ), + }, + plugins, + }) + .then(async (build) => { + await writeServerBuildResult(config, build.outputFiles); + return build; + }); +} + async function generateAssetsManifest( config: RemixConfig, - metafile: esbuild.Metafile, - cssModulesBuildPromiseRef: CssModulesBuildPromiseRef + build: BrowserBuild ): Promise { - let cssModulesResults = await cssModulesBuildPromiseRef.current; + let cssModulesResults: CssModulesResults = { + filePath: build.filePath, + fileUrl: build.fileUrl, + moduleMap: build.moduleMap, + }; let assetsManifest = await createAssetsManifest( config, - metafile, + build.result.metafile!, cssModulesResults ); let filename = `manifest-${assetsManifest.version.toUpperCase()}.js`; diff --git a/packages/remix-dev/compiler/plugins/cssModules.ts b/packages/remix-dev/compiler/plugins/cssModules.ts index ae8e410781a..2533170b789 100644 --- a/packages/remix-dev/compiler/plugins/cssModules.ts +++ b/packages/remix-dev/compiler/plugins/cssModules.ts @@ -8,8 +8,8 @@ import { getFileHash, getHash } from "../utils/crypto"; import * as cache from "../../cache"; import type { RemixConfig } from "../../config"; import { cssModulesVirtualModule } from "../virtualModules"; -import type { CssModulesBuild } from "../../compiler"; import { resolveUrl } from "../utils/url"; +import type { AssetsManifestPromiseRef } from "./serverAssetsManifestPlugin"; const suffixMatcher = /\.module\.css?$/; @@ -62,7 +62,7 @@ export function cssModulesPlugin( */ export function cssModulesFakerPlugin( config: RemixConfig, - cssModulesBuildPromiseRef: CssModulesBuildPromiseRef + assetsManifestPromiseRef: AssetsManifestPromiseRef ): esbuild.Plugin { return { name: "css-modules-faker", @@ -76,8 +76,8 @@ export function cssModulesFakerPlugin( }); build.onLoad({ filter: suffixMatcher }, async (args) => { - let res = await cssModulesBuildPromiseRef.current!; - let { json } = res.moduleMap[args.path]; + let res = await assetsManifestPromiseRef.current; + let json = res?.cssModules?.moduleMap[args.path].json || {}; return { contents: JSON.stringify(json), loader: "json", @@ -87,72 +87,6 @@ export function cssModulesFakerPlugin( }; } -/** - * Creates a virtual module called `@remix-run/dev/modules.css` that exports the - * URL of the compiled CSS that users will use in their route's `link` export. - */ -export function cssModulesVirtualModulePlugin( - cssModulesBuildPromiseRef: CssModulesBuildPromiseRef -): esbuild.Plugin { - let filter = cssModulesVirtualModule.filter; - return { - name: "css-modules-virtual-module", - setup(build) { - build.onResolve({ filter }, async ({ path }) => { - return { - path, - namespace: "css-modules-virtual-module", - }; - }); - - build.onLoad({ filter }, async () => { - let fileUrl = (await cssModulesBuildPromiseRef.current)?.fileUrl; - - return { - loader: "js", - contents: `export default ${ - fileUrl - ? `"${fileUrl}"` - : // If there is no CSS module file the import should return undefined. - // Users should check for a value and conditionally render a - // link only if we have modules. This way we can avoid annoying - // errors if they delete any references to CSS modules, though - // perhaps a dev warning would be helpful if they intended to - // remove CSS modules but left the virtual module import! - "undefined" - }`, - }; - }); - }, - }; -} - -/** - * We also need a 'faker' plugin for the virtual module since we don't know the - * URL yet. This is basically a noop that just feeds us an empty string since it - * doesn't really matter at this stage on the build. - */ -export function cssModulesVirtualModuleFakerPlugin(): esbuild.Plugin { - let filter = cssModulesVirtualModule.filter; - return { - name: "css-modules-virtual-module-faker", - setup(build) { - build.onResolve({ filter }, async ({ path }) => { - return { - path, - namespace: "css-modules-virtual-module", - }; - }); - build.onLoad({ filter }, async () => { - return { - loader: "js", - contents: `export default "";`, - }; - }); - }, - }; -} - async function processCssCached( config: RemixConfig, filePath: string @@ -244,10 +178,6 @@ export function getCssModulesFileReferences( return [filePath, fileUrl]; } -export interface CssModulesBuildPromiseRef { - current?: Promise; -} - export interface CssModuleFileContents { css: string; json: CssModuleClassMap; diff --git a/packages/remix-dev/modules.ts b/packages/remix-dev/modules.ts index 5cfff8fbfdb..621a50596c5 100644 --- a/packages/remix-dev/modules.ts +++ b/packages/remix-dev/modules.ts @@ -3,15 +3,17 @@ declare module "*.aac" { export default asset; } -// TODO: This isn't working right now because CSS modules still match `*.css` -// and I haven't yet figured out how to match all *except* for modules. -// See https://github.com/microsoft/TypeScript/issues/38638 -declare module "*.module.css" { - const classes: { readonly [key: string]: string }; - export default classes; -} declare module "*.css" { - const asset: string; + // This needs to be any because TS cannot differentiate between *.css & + // *.module.css. In an ideal world we could make it an object for modules and + // a string for regular CSS, but in practice I don't think the typing provides + // much value in either scenario. For users to get TS benefits w/ modules + // they'd need a declaration file for individual CSS module files. Would be + // nice if we could generate that for them. + // + // See https://github.com/microsoft/TypeScript/issues/38638 for TS limitations + // with wildcard matching. + const asset: any; export default asset; } declare module "*.eot" { From 44fee4770cef9dbb5def43ffd6b883061d4b6186 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Tue, 1 Mar 2022 15:26:50 -0800 Subject: [PATCH 12/71] cleanup --- packages/remix-dev/compiler.ts | 40 +++++++++---------- .../remix-dev/compiler/plugins/cssModules.ts | 1 - packages/remix-dev/compiler/virtualModules.ts | 5 --- 3 files changed, 20 insertions(+), 26 deletions(-) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index 9c21d9a5075..c159b9ee9f7 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -201,7 +201,7 @@ export async function watch( let browserBuildPromise = browserBuild.rebuild(); let assetsManifestPromise = browserBuildPromise.then((build) => - generateAssetsManifest(config, build) + generateAssetsManifest(config, build.metafile!, build.cssModules) ); // Assign the assetsManifestPromise to a ref so the server build can await @@ -294,8 +294,8 @@ interface BuildInvalidate { (): Promise; dispose(): void; } -export interface BrowserBuild extends CssModulesResults { - result: esbuild.BuildResult; +export interface BrowserBuild extends Omit { + cssModules: CssModulesResults; rebuild?: BuildInvalidate; } @@ -312,7 +312,7 @@ async function buildEverything( let browserBuildPromise = createBrowserBuild(config, options); let assetsManifestPromise = browserBuildPromise.then((build) => - generateAssetsManifest(config, build) + generateAssetsManifest(config, build.metafile!, build.cssModules) ); // Assign the assetsManifestPromise to a ref so the server build can await @@ -433,7 +433,7 @@ async function createBrowserBuild( await fse.writeFile(filePath, cssModulesContent); return { - result: build, + ...build, rebuild: (() => { if (!options.incremental) { return undefined; @@ -452,11 +452,13 @@ async function createBrowserBuild( await fse.writeFile(filePath, cssModulesContent); return { - result, + ...result, rebuild: builder, - filePath, - fileUrl, - moduleMap: cssModulesMap, + cssModules: { + filePath, + fileUrl, + moduleMap: cssModulesMap, + }, }; }) as BuildInvalidate; // TODO: unsure about this, check back w/ esbuild docs to clarify what @@ -464,9 +466,11 @@ async function createBrowserBuild( builder.dispose = build.rebuild!.dispose; return builder; })(), - filePath, - fileUrl, - moduleMap: cssModulesMap, + cssModules: { + filePath, + fileUrl, + moduleMap: cssModulesMap, + }, }; }); } @@ -552,17 +556,13 @@ async function createServerBuild( async function generateAssetsManifest( config: RemixConfig, - build: BrowserBuild + metafile: esbuild.Metafile, + cssModules: CssModulesResults | undefined ): Promise { - let cssModulesResults: CssModulesResults = { - filePath: build.filePath, - fileUrl: build.fileUrl, - moduleMap: build.moduleMap, - }; let assetsManifest = await createAssetsManifest( config, - build.result.metafile!, - cssModulesResults + metafile!, + cssModules ); let filename = `manifest-${assetsManifest.version.toUpperCase()}.js`; diff --git a/packages/remix-dev/compiler/plugins/cssModules.ts b/packages/remix-dev/compiler/plugins/cssModules.ts index 2533170b789..4abd23294ee 100644 --- a/packages/remix-dev/compiler/plugins/cssModules.ts +++ b/packages/remix-dev/compiler/plugins/cssModules.ts @@ -7,7 +7,6 @@ import type * as esbuild from "esbuild"; import { getFileHash, getHash } from "../utils/crypto"; import * as cache from "../../cache"; import type { RemixConfig } from "../../config"; -import { cssModulesVirtualModule } from "../virtualModules"; import { resolveUrl } from "../utils/url"; import type { AssetsManifestPromiseRef } from "./serverAssetsManifestPlugin"; diff --git a/packages/remix-dev/compiler/virtualModules.ts b/packages/remix-dev/compiler/virtualModules.ts index 2b77a8f4d48..e7df45fbd92 100644 --- a/packages/remix-dev/compiler/virtualModules.ts +++ b/packages/remix-dev/compiler/virtualModules.ts @@ -12,8 +12,3 @@ export const assetsManifestVirtualModule: VirtualModule = { id: "@remix-run/dev/assets-manifest", filter: /^@remix-run\/dev\/assets-manifest$/, }; - -export const cssModulesVirtualModule: VirtualModule = { - id: "@remix-run/dev/modules.css", - filter: /^@remix-run\/dev\/modules\.css$/, -}; From df50e1b77bf633f81ab0d1d9fda6a3d59e5ee244 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 4 Mar 2022 07:33:53 -0800 Subject: [PATCH 13/71] create @remix-run/css-modules package --- packages/remix-css-modules/README.md | 13 +++++++++++++ packages/remix-css-modules/__tests__/.gitkeep | 0 packages/remix-css-modules/index.ts | 1 + packages/remix-css-modules/package.json | 18 ++++++++++++++++++ packages/remix-css-modules/tsconfig.json | 17 +++++++++++++++++ 5 files changed, 49 insertions(+) create mode 100644 packages/remix-css-modules/README.md create mode 100644 packages/remix-css-modules/__tests__/.gitkeep create mode 100644 packages/remix-css-modules/index.ts create mode 100644 packages/remix-css-modules/package.json create mode 100644 packages/remix-css-modules/tsconfig.json diff --git a/packages/remix-css-modules/README.md b/packages/remix-css-modules/README.md new file mode 100644 index 00000000000..5c278710d2c --- /dev/null +++ b/packages/remix-css-modules/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)! diff --git a/packages/remix-css-modules/__tests__/.gitkeep b/packages/remix-css-modules/__tests__/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/remix-css-modules/index.ts b/packages/remix-css-modules/index.ts new file mode 100644 index 00000000000..cb0ff5c3b54 --- /dev/null +++ b/packages/remix-css-modules/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/remix-css-modules/package.json b/packages/remix-css-modules/package.json new file mode 100644 index 00000000000..d496b0eeb0d --- /dev/null +++ b/packages/remix-css-modules/package.json @@ -0,0 +1,18 @@ +{ + "name": "@remix-run/css-modules", + "description": "Entrypoint for stylesheets created by CSS Modules in Remix", + "version": "1.2.3", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/remix-run/remix", + "directory": "packages/remix-css-modules" + }, + "bugs": { + "url": "https://github.com/remix-run/remix/issues" + }, + "dependencies": { + "@remix-run/dev": "1.2.3", + "@remix-run/server-runtime": "1.2.3" + } +} diff --git a/packages/remix-css-modules/tsconfig.json b/packages/remix-css-modules/tsconfig.json new file mode 100644 index 00000000000..81243290f53 --- /dev/null +++ b/packages/remix-css-modules/tsconfig.json @@ -0,0 +1,17 @@ +{ + "exclude": ["__tests__"], + "compilerOptions": { + "lib": ["ES2019", "DOM.Iterable"], + "target": "ES2019", + + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "strict": true, + + "declaration": true, + "emitDeclarationOnly": true, + + "outDir": "../../build/node_modules/@remix-run/css-modules", + "rootDir": "." + } +} From 158ac8507144ecd4010ac662a9823de37e3805b8 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 4 Mar 2022 08:04:26 -0800 Subject: [PATCH 14/71] add css-modules package to build --- rollup.config.js | 59 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/rollup.config.js b/rollup.config.js index 1a1d4df01ee..0a81d10e84f 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -120,6 +120,64 @@ function remix() { ]; } +/** @returns {import("rollup").RollupOptions[]} */ +function remixCssModules() { + let sourceDir = "packages/remix-css-modules"; + let outputDir = "build/node_modules/@remix-run/css-modules"; + let version = getVersion(sourceDir); + + return [ + { + external(id) { + return isBareModuleId(id); + }, + input: [`${sourceDir}/index.ts`], + output: { + banner: createBanner("@remix-run/css-modules", 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}/index.ts`], + output: { + banner: createBanner("@remix-run/css-modules", version), + dir: `${outputDir}/esm`, + format: "esm", + preserveModules: true, + }, + plugins: [ + babel({ + babelHelpers: "bundled", + exclude: /node_modules/, + extensions: [".ts"], + }), + nodeResolve({ extensions: [".ts"] }), + ], + }, + ]; +} + /** @returns {import("rollup").RollupOptions[]} */ function remixDev() { let sourceDir = "packages/remix-dev"; @@ -834,6 +892,7 @@ export default function rollup(options) { let builds = [ ...createRemix(options), ...remix(options), + ...remixCssModules(options), ...remixDev(options), ...remixDeno(options), ...remixServerRuntime(options), From 2016b6d91bb3b48518680d4b2b5e2841c7793143 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 4 Mar 2022 08:04:38 -0800 Subject: [PATCH 15/71] add css-modules package to scripts --- scripts/publish.js | 12 ++++++++---- scripts/utils.js | 20 +++++++++++++++++++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/scripts/publish.js b/scripts/publish.js index e5f80ae65f4..c7b763c769c 100644 --- a/scripts/publish.js +++ b/scripts/publish.js @@ -27,9 +27,6 @@ async function run() { let prerelease = semver.prerelease(taggedVersion); let tag = prerelease ? prerelease[0] : "latest"; - // Publish eslint config directly from the package directory - publish(path.join(packageDir, "remix-eslint-config"), tag); - // Publish all @remix-run/* packages for (let name of [ "dev", @@ -44,8 +41,15 @@ async function run() { "netlify", "react", "serve", + "css-modules", + "eslint-config", ]) { - publish(path.join(buildDir, "@remix-run", name), tag); + if (name === "eslint-config") { + // Publish eslint config directly from the package directory + publish(path.join(packageDir, "remix-eslint-config"), tag); + } else { + publish(path.join(buildDir, "@remix-run", name), tag); + } } // Publish create-remix diff --git a/scripts/utils.js b/scripts/utils.js index 96faadf014b..e8812ad78cc 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -11,10 +11,28 @@ let examplesDir = path.resolve(rootDir, "examples"); let remixPackages = { adapters: ["architect", "express", "netlify", "vercel"], runtimes: ["cloudflare-workers", "cloudflare-pages", "deno", "node"], - core: ["dev", "server-runtime", "react", "eslint-config"], + core: ["dev", "server-runtime", "react", "css-modules", "eslint-config"], get all() { return [...this.adapters, ...this.runtimes, ...this.core, "serve"]; }, + + // Would be nice to keep this all in the same place as it's currently easy to + // forget to update this in the various places we need to handle our packages. + // + // TODO: Test when publishing with an experimental release to ensure no + // conflicts + get allForPublishing() { + return [ + "dev", + "server-runtime", // publish server-runtime before platforms + ...this.runtimes, // publish node before node servers + ...this.adapters, // publish express before serve + "react", + "serve", + "css-modules", + "eslint-config", + ]; + }, }; /** From fa86d32ecf36f5155b690b2f099f20a12a1a70d8 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 4 Mar 2022 09:03:50 -0800 Subject: [PATCH 16/71] update manifest types + resolve module imports --- packages/remix-dev/assets-manifest.d.ts | 5 +++ packages/remix-dev/compiler.ts | 32 ++++++++-------- .../remix-dev/compiler/plugins/cssModules.ts | 38 +++++++++++++++++-- packages/remix-dev/index.ts | 1 + packages/remix-react/entry.ts | 4 +- packages/remix-server-runtime/entry.ts | 4 +- rollup.config.js | 13 ++++++- 7 files changed, 72 insertions(+), 25 deletions(-) create mode 100644 packages/remix-dev/assets-manifest.d.ts diff --git a/packages/remix-dev/assets-manifest.d.ts b/packages/remix-dev/assets-manifest.d.ts new file mode 100644 index 00000000000..46d4b8d8a09 --- /dev/null +++ b/packages/remix-dev/assets-manifest.d.ts @@ -0,0 +1,5 @@ +declare module "@remix-run/dev/assets-manifest" { + import type { AssetsManifest } from "@remix-run/dev"; + const manifest: AssetsManifest; + export default manifest; +} diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index c159b9ee9f7..f6c6aa40763 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -30,6 +30,8 @@ import { cssModulesPlugin, cssModulesFakerPlugin, getCssModulesFileReferences, + browserCssModulesStylesheetPlugin, + serverCssModulesStylesheetPlugin, } from "./compiler/plugins/cssModules"; import { writeFileSafe } from "./compiler/utils/fs"; @@ -417,6 +419,7 @@ async function createBrowserBuild( }, plugins: [ cssModulesPlugin(config, handleProcessedCss), + browserCssModulesStylesheetPlugin(), mdxPlugin(config), browserRouteModulesPlugin(config, /\?browser$/), emptyModulesPlugin(config, /\.server(\.[jt]sx?)?$/), @@ -424,13 +427,11 @@ async function createBrowserBuild( ], }) .then(async (build) => { - let [filePath, fileUrl] = getCssModulesFileReferences( - config, - cssModulesContent - ); + let [globalStylesheetFilePath, globalStylesheetFileUrl] = + getCssModulesFileReferences(config, cssModulesContent); - await fse.ensureDir(path.dirname(filePath)); - await fse.writeFile(filePath, cssModulesContent); + await fse.ensureDir(path.dirname(globalStylesheetFilePath)); + await fse.writeFile(globalStylesheetFilePath, cssModulesContent); return { ...build, @@ -443,20 +444,18 @@ async function createBrowserBuild( cssModulesContent = ""; cssModulesMap = {}; let result = await build.rebuild!(); - let [filePath, fileUrl] = getCssModulesFileReferences( - config, - cssModulesContent - ); + let [globalStylesheetFilePath, globalStylesheetFileUrl] = + getCssModulesFileReferences(config, cssModulesContent); - await fse.ensureDir(path.dirname(filePath)); - await fse.writeFile(filePath, cssModulesContent); + await fse.ensureDir(path.dirname(globalStylesheetFilePath)); + await fse.writeFile(globalStylesheetFilePath, cssModulesContent); return { ...result, rebuild: builder, cssModules: { - filePath, - fileUrl, + globalStylesheetFilePath, + globalStylesheetFileUrl, moduleMap: cssModulesMap, }, }; @@ -467,8 +466,8 @@ async function createBrowserBuild( return builder; })(), cssModules: { - filePath, - fileUrl, + globalStylesheetFilePath, + globalStylesheetFileUrl, moduleMap: cssModulesMap, }, }; @@ -497,6 +496,7 @@ async function createServerBuild( let plugins: esbuild.Plugin[] = [ cssModulesFakerPlugin(config, assetsManifestPromiseRef), + serverCssModulesStylesheetPlugin(), mdxPlugin(config), emptyModulesPlugin(config, /\.client(\.[jt]sx?)?$/), serverRouteModulesPlugin(config), diff --git a/packages/remix-dev/compiler/plugins/cssModules.ts b/packages/remix-dev/compiler/plugins/cssModules.ts index 4abd23294ee..497655aed7d 100644 --- a/packages/remix-dev/compiler/plugins/cssModules.ts +++ b/packages/remix-dev/compiler/plugins/cssModules.ts @@ -69,7 +69,7 @@ export function cssModulesFakerPlugin( build.onResolve({ filter: suffixMatcher }, (args) => { return { path: getResolvedFilePath(config, args), - namespace: "css-modules", + namespace: "css-modules-faker", sideEffects: false, }; }); @@ -86,6 +86,36 @@ export function cssModulesFakerPlugin( }; } +export function serverCssModulesStylesheetPlugin(): esbuild.Plugin { + let filter = /^@remix-run\/css-modules$/; + return { + name: "server-css-modules-stylesheet", + async setup(build) { + build.onResolve({ filter }, ({ path }) => { + return { + path: path + "/server", + namespace: "server-css-modules-stylesheet", + }; + }); + }, + }; +} + +export function browserCssModulesStylesheetPlugin(): esbuild.Plugin { + let filter = /^@remix-run\/css-modules$/; + return { + name: "browser-css-modules-stylesheet", + async setup(build) { + build.onResolve({ filter }, ({ path }) => { + return { + path: path + "/browser", + namespace: "browser-css-modules-stylesheet", + }; + }); + }, + }; +} + async function processCssCached( config: RemixConfig, filePath: string @@ -166,7 +196,7 @@ function getResolvedFilePath( export function getCssModulesFileReferences( config: RemixConfig, css: string -): [filePath: string, fileUrl: string] { +): [globalStylesheetFilePath: string, globalStylesheetFileUrl: string] { let hash = getHash(css).slice(0, 8).toUpperCase(); let filePath = path.resolve( config.assetsBuildDirectory, @@ -185,8 +215,8 @@ export interface CssModuleFileContents { export type CssModuleFileMap = Record; export interface CssModulesResults { - filePath: string; - fileUrl: string; + globalStylesheetFilePath: string; + globalStylesheetFileUrl: string; moduleMap: CssModuleFileMap; } diff --git a/packages/remix-dev/index.ts b/packages/remix-dev/index.ts index 4d401d5c467..b479ee3ad5b 100644 --- a/packages/remix-dev/index.ts +++ b/packages/remix-dev/index.ts @@ -1,3 +1,4 @@ import "./modules"; export type { AppConfig } from "./config"; +export type { AssetsManifest } from "./compiler/assets"; diff --git a/packages/remix-react/entry.ts b/packages/remix-react/entry.ts index eacd5fc80d0..e02db95b80b 100644 --- a/packages/remix-react/entry.ts +++ b/packages/remix-react/entry.ts @@ -27,8 +27,8 @@ export interface AssetsManifest { // TODO: This should ideally be in one place; duped for now from remix-dev interface CssModulesResults { - filePath: string; - fileUrl: string; + globalStylesheetFilePath: string; + globalStylesheetFileUrl: string; moduleMap: CssModuleFileMap; } diff --git a/packages/remix-server-runtime/entry.ts b/packages/remix-server-runtime/entry.ts index 1ce158aa282..996ec7b7c42 100644 --- a/packages/remix-server-runtime/entry.ts +++ b/packages/remix-server-runtime/entry.ts @@ -32,8 +32,8 @@ export interface AssetsManifest { // TODO: This should ideally be in one place; duped for now from remix-dev interface CssModulesResults { - filePath: string; - fileUrl: string; + globalStylesheetFilePath: string; + globalStylesheetFileUrl: string; moduleMap: CssModuleFileMap; } diff --git a/rollup.config.js b/rollup.config.js index 0a81d10e84f..d6d30c8e162 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -129,9 +129,16 @@ function remixCssModules() { return [ { external(id) { + if (id === "@remix-run/dev/assets-manifest") { + return true; + } return isBareModuleId(id); }, - input: [`${sourceDir}/index.ts`], + input: [ + `${sourceDir}/index.ts`, + `${sourceDir}/browser.ts`, + `${sourceDir}/server.ts`, + ], output: { banner: createBanner("@remix-run/css-modules", version), dir: outputDir, @@ -157,6 +164,9 @@ function remixCssModules() { }, { external(id) { + if (id === "@remix-run/dev/assets-manifest") { + return true; + } return isBareModuleId(id); }, input: [`${sourceDir}/index.ts`], @@ -214,6 +224,7 @@ function remixDev() { { src: `LICENSE.md`, dest: outputDir }, { src: `${sourceDir}/package.json`, dest: outputDir }, { src: `${sourceDir}/README.md`, dest: outputDir }, + { src: `${sourceDir}/assets-manifest.d.ts`, dest: outputDir }, { src: `${sourceDir}/compiler/shims`, dest: `${outputDir}/compiler`, From 6653f17fe51bee65d1ad0d658ad612c2d057d554 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 4 Mar 2022 09:04:08 -0800 Subject: [PATCH 17/71] css-modules package entrypoints --- packages/remix-css-modules/browser.ts | 3 +++ packages/remix-css-modules/index.ts | 2 +- packages/remix-css-modules/server.ts | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 packages/remix-css-modules/browser.ts create mode 100644 packages/remix-css-modules/server.ts diff --git a/packages/remix-css-modules/browser.ts b/packages/remix-css-modules/browser.ts new file mode 100644 index 00000000000..312c01b16c1 --- /dev/null +++ b/packages/remix-css-modules/browser.ts @@ -0,0 +1,3 @@ +import type { AssetsManifest } from "@remix-run/dev"; +let assetsManifest: AssetsManifest = (window as any).__remixManifest; +export default assetsManifest.cssModules?.globalStylesheetFileUrl; diff --git a/packages/remix-css-modules/index.ts b/packages/remix-css-modules/index.ts index cb0ff5c3b54..bd835164538 100644 --- a/packages/remix-css-modules/index.ts +++ b/packages/remix-css-modules/index.ts @@ -1 +1 @@ -export {}; +export { default } from "./server"; diff --git a/packages/remix-css-modules/server.ts b/packages/remix-css-modules/server.ts new file mode 100644 index 00000000000..278c72dbc52 --- /dev/null +++ b/packages/remix-css-modules/server.ts @@ -0,0 +1,2 @@ +import assetsManifest from "@remix-run/dev/assets-manifest"; +export default assetsManifest.cssModules?.globalStylesheetFileUrl; From 01ee119e302cb0546c0344d19a9687f731320db4 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 4 Mar 2022 14:22:02 -0800 Subject: [PATCH 18/71] add css-modules to workspaces --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index a85f6430f2b..deac253b926 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "packages/remix-architect", "packages/remix-cloudflare-pages", "packages/remix-cloudflare-workers", + "packages/remix-css-modules", "packages/remix-deno", "packages/remix-dev", "packages/remix-eslint-config", From a66e67c74e81871e729cb2a6f2f74e597b7b0968 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 4 Mar 2022 14:23:55 -0800 Subject: [PATCH 19/71] rm index and fix package.json --- packages/remix-css-modules/index.ts | 1 - packages/remix-css-modules/package.json | 7 +++++++ rollup.config.js | 14 ++------------ 3 files changed, 9 insertions(+), 13 deletions(-) delete mode 100644 packages/remix-css-modules/index.ts diff --git a/packages/remix-css-modules/index.ts b/packages/remix-css-modules/index.ts deleted file mode 100644 index bd835164538..00000000000 --- a/packages/remix-css-modules/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./server"; diff --git a/packages/remix-css-modules/package.json b/packages/remix-css-modules/package.json index d496b0eeb0d..bb3453b31ac 100644 --- a/packages/remix-css-modules/package.json +++ b/packages/remix-css-modules/package.json @@ -3,6 +3,12 @@ "description": "Entrypoint for stylesheets created by CSS Modules in Remix", "version": "1.2.3", "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", @@ -12,6 +18,7 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { + "@parcel/css": "^1.5.0", "@remix-run/dev": "1.2.3", "@remix-run/server-runtime": "1.2.3" } diff --git a/rollup.config.js b/rollup.config.js index d6d30c8e162..0793a8ef01b 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -129,16 +129,9 @@ function remixCssModules() { return [ { external(id) { - if (id === "@remix-run/dev/assets-manifest") { - return true; - } return isBareModuleId(id); }, - input: [ - `${sourceDir}/index.ts`, - `${sourceDir}/browser.ts`, - `${sourceDir}/server.ts`, - ], + input: [`${sourceDir}/browser.ts`, `${sourceDir}/server.ts`], output: { banner: createBanner("@remix-run/css-modules", version), dir: outputDir, @@ -164,12 +157,9 @@ function remixCssModules() { }, { external(id) { - if (id === "@remix-run/dev/assets-manifest") { - return true; - } return isBareModuleId(id); }, - input: [`${sourceDir}/index.ts`], + input: [`${sourceDir}/browser.ts`, `${sourceDir}/server.ts`], output: { banner: createBanner("@remix-run/css-modules", version), dir: `${outputDir}/esm`, From bb050fcff06038dea2f3c884e0b2e58449a308b6 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 4 Mar 2022 14:24:06 -0800 Subject: [PATCH 20/71] add css-modules to tsconfig --- tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsconfig.json b/tsconfig.json index a751ca79507..34f1102e7b1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ { "path": "packages/remix-architect" }, { "path": "packages/remix-cloudflare-pages" }, { "path": "packages/remix-cloudflare-workers" }, + { "path": "packages/remix-css-modules" }, { "path": "packages/remix-deno" }, { "path": "packages/remix-dev" }, { "path": "packages/remix-express" }, From a6079494e4d0b309bb287d8dad2d9c2ca9ec5afc Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 4 Mar 2022 14:39:43 -0800 Subject: [PATCH 21/71] add processor for Parcel --- packages/remix-dev/compiler.ts | 11 +- .../remix-dev/compiler/plugins/cssModules.ts | 176 ++++++++++++++++-- packages/remix-dev/package.json | 1 + 3 files changed, 170 insertions(+), 18 deletions(-) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index f6c6aa40763..a2ffeacfe78 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -343,11 +343,14 @@ async function createBrowserBuild( ): Promise { let cssModulesContent = ""; let cssModulesJson: CssModuleClassMap = {}; - let cssModulesMap: Record = - {}; + let cssModulesMap: Record< + string, + { css: string; json: CssModuleClassMap; sourceMap: string | null } + > = {}; function handleProcessedCss( filePath: string, css: string, + sourceMap: string | null, json: CssModuleClassMap ) { cssModulesContent += css; @@ -357,6 +360,8 @@ async function createBrowserBuild( [filePath]: { css, json, + // TODO: Implement sourcemaps + sourceMap: null, }, }; } @@ -419,7 +424,6 @@ async function createBrowserBuild( }, plugins: [ cssModulesPlugin(config, handleProcessedCss), - browserCssModulesStylesheetPlugin(), mdxPlugin(config), browserRouteModulesPlugin(config, /\?browser$/), emptyModulesPlugin(config, /\.server(\.[jt]sx?)?$/), @@ -496,7 +500,6 @@ async function createServerBuild( let plugins: esbuild.Plugin[] = [ cssModulesFakerPlugin(config, assetsManifestPromiseRef), - serverCssModulesStylesheetPlugin(), mdxPlugin(config), emptyModulesPlugin(config, /\.client(\.[jt]sx?)?$/), serverRouteModulesPlugin(config), diff --git a/packages/remix-dev/compiler/plugins/cssModules.ts b/packages/remix-dev/compiler/plugins/cssModules.ts index 497655aed7d..59e53c9bc56 100644 --- a/packages/remix-dev/compiler/plugins/cssModules.ts +++ b/packages/remix-dev/compiler/plugins/cssModules.ts @@ -1,6 +1,7 @@ import postcss from "postcss"; import cssModules from "postcss-modules"; import path from "path"; +import chalk from "chalk"; import * as fse from "fs-extra"; import type * as esbuild from "esbuild"; @@ -12,6 +13,14 @@ import type { AssetsManifestPromiseRef } from "./serverAssetsManifestPlugin"; const suffixMatcher = /\.module\.css?$/; +// TODO: Remove when finished comparing Parcel + PostCSS +const USE_PARCEL = false; + +let ParcelCSS: { + transform(opts: ParcelTransformOptions): ParcelTransformResult; +}; +const decoder = new TextDecoder(); + /** * Loads *.module.css files and returns the hashed JSON so we can get the right * classnames in the HTML. @@ -21,13 +30,36 @@ export function cssModulesPlugin( handleProcessedCss: ( filePath: string, css: string, + sourceMap: string | null, json: CssModuleClassMap ) => void ): esbuild.Plugin { return { name: "css-modules", async setup(build) { - build.onResolve({ filter: suffixMatcher }, (args) => { + build.onResolve({ filter: suffixMatcher }, async (args) => { + if (USE_PARCEL) { + try { + if (!ParcelCSS) { + ParcelCSS = (await import("@remix-run/css-modules")).ParcelCSS; + console.warn( + chalk.yellow(`CSS Modules support in Remix is experimental. It's implementation may change. If you find a bug, please report it by opening an issue on GitHub: + + https://github.com/remix-run/remix/issues/new?labels=bug&template=bug_report.yml`) + ); + } + } catch (_) { + throw _; + // throw Error( + // `A CSS Modules file was imported, but the required \`@remix-run/css-modules\` dependency was not found. + + // Install the dependency by running the following command and restart your app. + + // npm install @remix-run/css-modules` + // ); + } + } + let path = getResolvedFilePath(config, args); return { path, @@ -38,8 +70,11 @@ export function cssModulesPlugin( build.onLoad({ filter: suffixMatcher }, async (args) => { try { - let { css, json } = await processCssCached(config, args.path); - handleProcessedCss(args.path, css, json); + let { css, json, sourceMap } = await processCssCached( + config, + args.path + ); + handleProcessedCss(args.path, css, sourceMap, json); return { contents: JSON.stringify(json), loader: "json", @@ -135,8 +170,8 @@ async function processCssCached( } if (!cached || cached.hash !== hash) { - let { css, json } = await processCss(filePath); - cached = { hash, css, json }; + let { css, json, sourceMap } = await processCss(config, filePath); + cached = { hash, css, json, sourceMap }; try { await cache.putJson(config.cacheDirectory, key, cached); @@ -145,19 +180,18 @@ async function processCssCached( } } - return { - css: cached.css, - json: cached.json, - }; + return cached; })(); return processedCssPromise; } -async function processCss(file: string) { +async function processCssWithPostCss( + file: string +): Promise { let json: CssModuleClassMap = {}; let source = await fse.readFile(file, "utf-8"); - let { css } = await postcss([ + let { css, map: mapRaw } = await postcss([ cssModules({ localsConvention: "camelCase", // [name] -> CSS modules file-name (button.module.css -> button-module) @@ -174,11 +208,62 @@ async function processCss(file: string) { }), ]).process(source, { from: undefined, - map: false, + map: true, }); - // TODO: Support sourcemaps when using .module.css files - return { css, json }; + let sourceMap = mapRaw ? mapRaw.toString() : null; + return { css, json, sourceMap }; +} + +async function processCssWithParcel( + config: RemixConfig, + file: string +): Promise { + let json: CssModuleClassMap = {}; + let source = await fse.readFile(file); + + let res = ParcelCSS.transform({ + filename: path.relative(config.appDirectory, file), + code: source, + cssModules: true, + minify: process.env.NODE_ENV === "production", + // Users will not be able to @import other stylesheets in modules with this + // limitation, nor can you compose classes from outside stylesheets as we'd + // have to decide where and how we want to handle duplicate dependencies in + // various stylesheets. This is not a feature in CSS Modules specifically, + // but other frameworks may support it. We might want to do more research + // here, but in the mean time any dependencies should be imported as a + // separate global stylesheet and loaded before the CSS Modules stylesheet. + analyzeDependencies: false, + sourceMap: true, + drafts: { + nesting: true, + }, + }); + + let parcelExports = res.exports || {}; + for (let key in parcelExports) { + let props = parcelExports[key]; + json = { + ...json, + [key]: props.composes.length + ? getComposedClassNames(props.name, props.composes) + : props.name, + }; + } + let css = decoder.decode(res.code); + let sourceMap = res.map ? decoder.decode(res.map) : null; + + return { css, json, sourceMap }; +} + +async function processCss( + config: RemixConfig, + file: string +): Promise { + return await (USE_PARCEL + ? processCssWithParcel(config, file) + : processCssWithPostCss(file)); } function getResolvedFilePath( @@ -207,9 +292,25 @@ export function getCssModulesFileReferences( return [filePath, fileUrl]; } +/** + * When a user composes classnames in CSS modules, the value returned for the + * JSON map is a concat'd version of all the classname strings composed. Note + * that the user may compose classnames referenced in other CSS module files, + * but that will require us to juggle dependencies and we're not quite ready for + * that yet. Will revisit that later. + */ +function getComposedClassNames(name: string, composes: ParcelComposeData[]) { + return composes.reduce((prev, cur) => { + // skip dependencies for now + if (cur.type === "dependency") return prev; + return prev + " " + cur.name; + }, name); +} + export interface CssModuleFileContents { css: string; json: CssModuleClassMap; + sourceMap: string | null; } export type CssModuleFileMap = Record; @@ -221,3 +322,50 @@ export interface CssModulesResults { } export type CssModuleClassMap = Record; + +// Copy/pasted some types to avoid imports since we're doing that dynamically +interface ParcelTransformOptions { + filename: string; + code: Buffer; + minify?: boolean; + sourceMap?: boolean; + targets?: ParcelTargets; + cssModules?: boolean; + drafts?: { [key: string]: boolean }; + analyzeDependencies?: boolean; + unusedSymbols?: string[]; +} + +interface ParcelTargets { + android?: number; + chrome?: number; + edge?: number; + firefox?: number; + ie?: number; + ios_saf?: number; + opera?: number; + safari?: number; + samsung?: number; +} + +interface ParcelTransformResult { + code: Buffer; + map: Buffer | void; + exports: ParcelCSSModuleExports | void; + dependencies: any[] | void; +} + +type ParcelCSSModuleExports = { + [name: string]: ParcelCSSModuleExport; +}; + +interface ParcelCSSModuleExport { + name: string; + isReferenced: boolean; + composes: ParcelComposeData[]; +} + +interface ParcelComposeData { + type: "local" | "global" | "dependency"; + name: string; +} diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 8577d1e4d7a..e2eadd0873b 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -18,6 +18,7 @@ "@esbuild-plugins/node-modules-polyfill": "^0.1.4", "@remix-run/server-runtime": "1.2.3", "cacache": "^15.0.5", + "chalk": "^4.1.0", "chokidar": "^3.5.1", "dotenv": "^16.0.0", "esbuild": "0.14.22", From 91c0c8eb2b06b84ce27fdbe91470039b75f030e0 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 4 Mar 2022 14:40:31 -0800 Subject: [PATCH 22/71] bundle internal css-modules package --- .../remix-dev/compiler/plugins/serverBareModulesPlugin.ts | 5 +++++ packages/remix-react/entry.ts | 1 + 2 files changed, 6 insertions(+) diff --git a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts index 174e1bdc117..b11f27606ec 100644 --- a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts +++ b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts @@ -27,6 +27,11 @@ export function serverBareModulesPlugin( return undefined; } + // Always bundle @remix-run/css-modules + if (path === "@remix-run/css-modules") { + return undefined; + } + // To prevent `import xxx from "remix"` from ending up in the bundle // we "bundle" remix but the other modules where the code lives. if (path === "remix") { diff --git a/packages/remix-react/entry.ts b/packages/remix-react/entry.ts index e02db95b80b..ee9491266f0 100644 --- a/packages/remix-react/entry.ts +++ b/packages/remix-react/entry.ts @@ -34,6 +34,7 @@ interface CssModulesResults { interface CssModuleFileContents { css: string; + sourceMap: string | null; json: CssModuleClassMap; } From 4952de5e5314e7a0ccb1fc36d27ffca948e3f1a3 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 4 Mar 2022 14:41:46 -0800 Subject: [PATCH 23/71] add css modules to gists app --- .../app/components/Counter.module.css | 2 +- fixtures/gists-app/app/root.jsx | 13 +++- fixtures/gists-app/remix.config.js | 1 + yarn.lock | 61 +++++++++++++++++++ 4 files changed, 74 insertions(+), 3 deletions(-) diff --git a/fixtures/gists-app/app/components/Counter.module.css b/fixtures/gists-app/app/components/Counter.module.css index dc0521e7a93..54df30bb3c3 100644 --- a/fixtures/gists-app/app/components/Counter.module.css +++ b/fixtures/gists-app/app/components/Counter.module.css @@ -1,3 +1,3 @@ .button { - color: auto; + color: white; } diff --git a/fixtures/gists-app/app/root.jsx b/fixtures/gists-app/app/root.jsx index 031ca3c267a..224463a7bad 100644 --- a/fixtures/gists-app/app/root.jsx +++ b/fixtures/gists-app/app/root.jsx @@ -9,10 +9,10 @@ import { useCatch, useLoaderData, useMatches, + LiveReload, } from "remix"; import normalizeHref from "@exampledev/new.css/new.css"; -// TODO: This will be a separate module now instead of the virtual module -// import cssModuleStyles from "@remix-run/dev/modules.css"; +import cssModuleStylesheetUrl from "@remix-run/css-modules"; import favicon from "../public/favicon.ico"; import stylesHref from "./styles/app.css"; @@ -24,6 +24,7 @@ export function links() { href: normalizeHref, }, { rel: "stylesheet", href: stylesHref }, + { rel: "stylesheet", href: cssModuleStylesheetUrl }, { rel: "stylesheet", href: "/resources/theme-css" }, // cssModuleStyles != null && { rel: "stylesheet", href: cssModuleStyles }, { rel: "shortcut icon", href: favicon }, @@ -31,6 +32,7 @@ export function links() { } export async function loader({ request }) { + console.log({ server: cssModuleStylesheetUrl }); return { enableScripts: new URL(request.url).searchParams.get("disableJs") == null, }; @@ -48,6 +50,10 @@ export default function Root() { window.reactIsHydrated = true; }); + useEffect(() => { + console.log({ client: cssModuleStylesheetUrl }); + }); + let data = useLoaderData(); let matches = useMatches(); @@ -75,6 +81,7 @@ export default function Root() { <> + ) : null} @@ -129,6 +136,7 @@ export function CatchBoundary() { ) : null} + ); @@ -156,6 +164,7 @@ export function ErrorBoundary({ error }) {
{error.message}
+ ); diff --git a/fixtures/gists-app/remix.config.js b/fixtures/gists-app/remix.config.js index 5ba9a122527..f6eaec9e033 100644 --- a/fixtures/gists-app/remix.config.js +++ b/fixtures/gists-app/remix.config.js @@ -12,6 +12,7 @@ module.exports = { devServerPort: 8002, ignoredRouteFiles: [".*", "blargh.ts"], server: "./server.js", + devServerBroadcastDelay: 500, mdx: async (filename) => { const [rehypeHighlight, remarkToc] = await Promise.all([ diff --git a/yarn.lock b/yarn.lock index 12062aa00fb..b262502fdb5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1762,6 +1762,62 @@ mkdirp "^1.0.4" rimraf "^3.0.2" +"@parcel/css-darwin-arm64@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@parcel/css-darwin-arm64/-/css-darwin-arm64-1.5.0.tgz#1eab3825953cd1b95cdd41b71991992b36814bf4" + integrity sha512-5JwRRADWu7dWRvN0XbUGTWnTNOQaCCxWtXRRPSS9hDQnc337eCjfQknRPX+CSh1uzX+LJu37xfYF2odzkjpFTQ== + +"@parcel/css-darwin-x64@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@parcel/css-darwin-x64/-/css-darwin-x64-1.5.0.tgz#3454efa0885856c7a3ca6b1d08e653e4e1306c80" + integrity sha512-dJgoLl+9kEBzXHnYG1XxyUSXxOceQgK/19/E11XBEVxRIF1yBHxhmpFuJaTVCIJbsotYKLr+H+CmOA0dgfKJcQ== + +"@parcel/css-linux-arm-gnueabihf@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@parcel/css-linux-arm-gnueabihf/-/css-linux-arm-gnueabihf-1.5.0.tgz#83b2a88d2e88373fff71254fa1671e9b85d60e1d" + integrity sha512-PiasN1xjWp+RI3sczc+Kfg4W6jnD9k7Zpz4qLRamUvZS5NE31X3rKsxkJlaNLjXM2RfeaWhcxEV87isMKRK9TQ== + +"@parcel/css-linux-arm64-gnu@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@parcel/css-linux-arm64-gnu/-/css-linux-arm64-gnu-1.5.0.tgz#f1900fc05823e551a2dae4287b22542a327f5f41" + integrity sha512-mKJeC5p3E4z3XbRi9iBcdOe8OYAwI1CAdOmf3lKfWr1m1vuAFUG2D/YykShZUTbPRnW+m1DFuAVHXcv0zLuxww== + +"@parcel/css-linux-arm64-musl@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@parcel/css-linux-arm64-musl/-/css-linux-arm64-musl-1.5.0.tgz#24987f0fd949f3cfcd1c5554a87dcf4ed57153b9" + integrity sha512-sZPaVtnK/RG1CStBJzNMxjcZasb/veuvaq+GbcZaOTAHKA+fzNFWrSgM/1xHDdVOk8boVWT4Vn3AUgZrrbqmEQ== + +"@parcel/css-linux-x64-gnu@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@parcel/css-linux-x64-gnu/-/css-linux-x64-gnu-1.5.0.tgz#b345b649d8e353f3600b68089c1db18dee5a74bf" + integrity sha512-dDkbsx9S/EE3Sph9Az3PAFOgi2q2xSmRdME4PgnXGsux6R2hHwG+RGX0V406XyxTkIL4Gt8Vmmr09T0y+cdGSw== + +"@parcel/css-linux-x64-musl@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@parcel/css-linux-x64-musl/-/css-linux-x64-musl-1.5.0.tgz#554518576f856660a6f25379297505142d9dab61" + integrity sha512-5Xpd+zu6OeI3h8Jk0AWpZwQkXbmlxNP+Jn+1yxec1wBO03Ke58r0glcyiaFSYjJeYTgWK9Rl/M5IucIKy4bvzg== + +"@parcel/css-win32-x64-msvc@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@parcel/css-win32-x64-msvc/-/css-win32-x64-msvc-1.5.0.tgz#ea43bdd325b23db4b68b1fe3ab9e04395b38ba6b" + integrity sha512-wp5/n4HEXp61MKyrPmSPOg3UEO4rE5BPT8Ab7jv+jML0X978vurBOaT0pH57z1MFNmLwCkCylDx0JiggjZvPMg== + +"@parcel/css@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@parcel/css/-/css-1.5.0.tgz#d288a5d8b8db1b6e7805ba034754be855015e7f3" + integrity sha512-nfZDYfxL60RNmST6fvlfIdulZnoPDrkce+qxHHZXvvoxjDrdn6T4Z1bz7QB9EmCJHcyNpDKEmkZTrBX0skUGbQ== + dependencies: + detect-libc "^1.0.3" + optionalDependencies: + "@parcel/css-darwin-arm64" "1.5.0" + "@parcel/css-darwin-x64" "1.5.0" + "@parcel/css-linux-arm-gnueabihf" "1.5.0" + "@parcel/css-linux-arm64-gnu" "1.5.0" + "@parcel/css-linux-arm64-musl" "1.5.0" + "@parcel/css-linux-x64-gnu" "1.5.0" + "@parcel/css-linux-x64-musl" "1.5.0" + "@parcel/css-win32-x64-msvc" "1.5.0" + "@rollup/plugin-babel@^5.2.2": version "5.3.0" resolved "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz" @@ -4254,6 +4310,11 @@ detect-indent@^6.0.0: resolved "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= + detect-newline@3.1.0, detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" From 9054616070e29c632f9ccdbd31caacc3aea120dd Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 4 Mar 2022 16:56:51 -0800 Subject: [PATCH 24/71] check composes feature --- fixtures/gists-app/app/components/Counter.js | 2 +- fixtures/gists-app/app/components/Counter.module.css | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/fixtures/gists-app/app/components/Counter.js b/fixtures/gists-app/app/components/Counter.js index bdd3cc4ede3..ddcbca1f55f 100644 --- a/fixtures/gists-app/app/components/Counter.js +++ b/fixtures/gists-app/app/components/Counter.js @@ -10,7 +10,7 @@ export default function Counter() { onClick={() => setCount(count + 1)} className={styles.button} > - {`Clicked ${count}`} + {`Clicked ${count}`} ); } diff --git a/fixtures/gists-app/app/components/Counter.module.css b/fixtures/gists-app/app/components/Counter.module.css index 54df30bb3c3..1d66f1440a9 100644 --- a/fixtures/gists-app/app/components/Counter.module.css +++ b/fixtures/gists-app/app/components/Counter.module.css @@ -1,3 +1,8 @@ .button { color: white; } + +.inner { + composes: button; + font-weight: bold; +} From b2000365775334e2f6aa5dde1bcf3bd61a1c5985 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 4 Mar 2022 16:57:24 -0800 Subject: [PATCH 25/71] deps --- packages/remix-css-modules/package.json | 4 +--- packages/remix-dev/assets-manifest.d.ts | 9 ++++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/remix-css-modules/package.json b/packages/remix-css-modules/package.json index bb3453b31ac..3eda79cc17d 100644 --- a/packages/remix-css-modules/package.json +++ b/packages/remix-css-modules/package.json @@ -18,8 +18,6 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@parcel/css": "^1.5.0", - "@remix-run/dev": "1.2.3", - "@remix-run/server-runtime": "1.2.3" + "@parcel/css": "^1.5.0" } } diff --git a/packages/remix-dev/assets-manifest.d.ts b/packages/remix-dev/assets-manifest.d.ts index 46d4b8d8a09..7526558f4bc 100644 --- a/packages/remix-dev/assets-manifest.d.ts +++ b/packages/remix-dev/assets-manifest.d.ts @@ -1,5 +1,4 @@ -declare module "@remix-run/dev/assets-manifest" { - import type { AssetsManifest } from "@remix-run/dev"; - const manifest: AssetsManifest; - export default manifest; -} +import type { AssetsManifest } from "@remix-run/dev"; +declare const manifest: AssetsManifest; +export type { AssetsManifest }; +export default manifest; From e0085c52ea444a59d5e8e5fc66ea52b2541b0ed9 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 4 Mar 2022 16:57:36 -0800 Subject: [PATCH 26/71] fix browser import --- packages/remix-css-modules/browser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/remix-css-modules/browser.ts b/packages/remix-css-modules/browser.ts index 312c01b16c1..da68e4bdbfc 100644 --- a/packages/remix-css-modules/browser.ts +++ b/packages/remix-css-modules/browser.ts @@ -1,3 +1,3 @@ -import type { AssetsManifest } from "@remix-run/dev"; +import type { AssetsManifest } from "@remix-run/dev/assets-manifest"; let assetsManifest: AssetsManifest = (window as any).__remixManifest; export default assetsManifest.cssModules?.globalStylesheetFileUrl; From 770d62d98c3925e50aa253d753331c393601e8bb Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 4 Mar 2022 17:13:22 -0800 Subject: [PATCH 27/71] types and such --- packages/remix-css-modules/tsconfig.json | 1 + packages/remix-css-modules/types.ts | 75 +++++++++++++++++++ .../remix-dev/compiler/plugins/cssModules.ts | 31 +++++--- 3 files changed, 97 insertions(+), 10 deletions(-) create mode 100644 packages/remix-css-modules/types.ts diff --git a/packages/remix-css-modules/tsconfig.json b/packages/remix-css-modules/tsconfig.json index 81243290f53..2a86d040db4 100644 --- a/packages/remix-css-modules/tsconfig.json +++ b/packages/remix-css-modules/tsconfig.json @@ -4,6 +4,7 @@ "lib": ["ES2019", "DOM.Iterable"], "target": "ES2019", + "module": "CommonJS", "moduleResolution": "node", "allowSyntheticDefaultImports": true, "strict": true, diff --git a/packages/remix-css-modules/types.ts b/packages/remix-css-modules/types.ts new file mode 100644 index 00000000000..9294c2ec2ff --- /dev/null +++ b/packages/remix-css-modules/types.ts @@ -0,0 +1,75 @@ +/** + * Many of these types were copied from @parcel/css and modified slightly for + * our needs. Some of this data will end up in the assets manifest but we don't + * want more than we need. + */ + +export type CssModuleExports = { + [name: string]: CssModuleExport; +}; + +export interface CssModuleExport { + name: string; + composes: CssModuleReference[]; +} + +export type CssModuleReference = + | LocalCssModuleReference + | GlobalCssModuleReference + | DependencyCssModuleReference; + +export interface LocalCssModuleReference { + type: "local"; + name: string; +} + +export interface GlobalCssModuleReference { + type: "global"; + name: string; +} + +export interface DependencyCssModuleReference { + type: "dependency"; + name: string; + /** The dependency specifier for the referenced file. */ + specifier: string; +} + +export type Dependency = ImportDependency | UrlDependency; + +export interface ImportDependency { + type: "import"; + /** The url of the `@import` dependency. */ + url: string; + /** The media query for the `@import` rule. */ + media: string | null; + /** The `supports()` query for the `@import` rule. */ + supports: string | null; + /** The source location where the `@import` rule was found. */ + loc: SourceLocation; +} + +export interface UrlDependency { + type: "url"; + /** The url of the dependency. */ + url: string; + /** The source location where the `url()` was found. */ + loc: SourceLocation; + /** The placeholder that the url was replaced with. */ + placeholder: string; +} + +interface SourceLocation { + /** The file path in which the dependency exists. */ + filePath: string; + /** The start location of the dependency. */ + start: { + line: number; + column: number; + }; + /** The end location (inclusive) of the dependency. */ + end: { + line: number; + column: number; + }; +} diff --git a/packages/remix-dev/compiler/plugins/cssModules.ts b/packages/remix-dev/compiler/plugins/cssModules.ts index 59e53c9bc56..4bdea011d65 100644 --- a/packages/remix-dev/compiler/plugins/cssModules.ts +++ b/packages/remix-dev/compiler/plugins/cssModules.ts @@ -14,11 +14,9 @@ import type { AssetsManifestPromiseRef } from "./serverAssetsManifestPlugin"; const suffixMatcher = /\.module\.css?$/; // TODO: Remove when finished comparing Parcel + PostCSS -const USE_PARCEL = false; +const USE_PARCEL = true; -let ParcelCSS: { - transform(opts: ParcelTransformOptions): ParcelTransformResult; -}; +let parcelTransform: (opts: ParcelTransformOptions) => ParcelTransformResult; const decoder = new TextDecoder(); /** @@ -40,12 +38,20 @@ export function cssModulesPlugin( build.onResolve({ filter: suffixMatcher }, async (args) => { if (USE_PARCEL) { try { - if (!ParcelCSS) { - ParcelCSS = (await import("@remix-run/css-modules")).ParcelCSS; + if (!parcelTransform) { + parcelTransform = (await import("@parcel/css")).default.transform; + console.log({ parcelTransform }); console.warn( - chalk.yellow(`CSS Modules support in Remix is experimental. It's implementation may change. If you find a bug, please report it by opening an issue on GitHub: + chalk.yellow(` +-------------------------------------------------------------------------------- + +CSS Modules support in Remix is experimental. It's implementation may change. +If you find a bug, please report it by opening an issue on GitHub: - https://github.com/remix-run/remix/issues/new?labels=bug&template=bug_report.yml`) +https://github.com/remix-run/remix/issues/new?labels=bug&template=bug_report.yml + +-------------------------------------------------------------------------------- +`) ); } } catch (_) { @@ -222,7 +228,7 @@ async function processCssWithParcel( let json: CssModuleClassMap = {}; let source = await fse.readFile(file); - let res = ParcelCSS.transform({ + let res = parcelTransform({ filename: path.relative(config.appDirectory, file), code: source, cssModules: true, @@ -234,7 +240,7 @@ async function processCssWithParcel( // but other frameworks may support it. We might want to do more research // here, but in the mean time any dependencies should be imported as a // separate global stylesheet and loaded before the CSS Modules stylesheet. - analyzeDependencies: false, + analyzeDependencies: true, sourceMap: true, drafts: { nesting: true, @@ -369,3 +375,8 @@ interface ParcelComposeData { type: "local" | "global" | "dependency"; name: string; } + +interface ParcelComposeData { + type: "local" | "global" | "dependency"; + name: string; +} From 5ecd50af3ab7aac98ff60d8e83c4d60cae0c2140 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 4 Mar 2022 18:11:03 -0800 Subject: [PATCH 28/71] types --- packages/remix-css-modules/types.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/remix-css-modules/types.ts b/packages/remix-css-modules/types.ts index 9294c2ec2ff..a2c896b15f0 100644 --- a/packages/remix-css-modules/types.ts +++ b/packages/remix-css-modules/types.ts @@ -28,16 +28,18 @@ export interface GlobalCssModuleReference { name: string; } -export interface DependencyCssModuleReference { +interface DependencyCssModuleReference { type: "dependency"; name: string; /** The dependency specifier for the referenced file. */ specifier: string; } -export type Dependency = ImportDependency | UrlDependency; +export type CssModuleDependency = + | CssModuleImportDependency + | CssModuleUrlDependency; -export interface ImportDependency { +interface CssModuleImportDependency { type: "import"; /** The url of the `@import` dependency. */ url: string; @@ -46,20 +48,20 @@ export interface ImportDependency { /** The `supports()` query for the `@import` rule. */ supports: string | null; /** The source location where the `@import` rule was found. */ - loc: SourceLocation; + loc: CssModuleSourceLocation; } -export interface UrlDependency { +interface CssModuleUrlDependency { type: "url"; /** The url of the dependency. */ url: string; /** The source location where the `url()` was found. */ - loc: SourceLocation; + loc: CssModuleSourceLocation; /** The placeholder that the url was replaced with. */ placeholder: string; } -interface SourceLocation { +interface CssModuleSourceLocation { /** The file path in which the dependency exists. */ filePath: string; /** The start location of the dependency. */ From 0d48c47c80a3665c67cdf2f27eaf6610225f282e Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 4 Mar 2022 18:11:19 -0800 Subject: [PATCH 29/71] clean up gists app --- fixtures/gists-app/app/components/Counter.module.css | 2 +- fixtures/gists-app/app/root.jsx | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/fixtures/gists-app/app/components/Counter.module.css b/fixtures/gists-app/app/components/Counter.module.css index 1d66f1440a9..349d5bb7d83 100644 --- a/fixtures/gists-app/app/components/Counter.module.css +++ b/fixtures/gists-app/app/components/Counter.module.css @@ -1,5 +1,5 @@ .button { - color: white; + color: yellow; } .inner { diff --git a/fixtures/gists-app/app/root.jsx b/fixtures/gists-app/app/root.jsx index 224463a7bad..ab86e1706b0 100644 --- a/fixtures/gists-app/app/root.jsx +++ b/fixtures/gists-app/app/root.jsx @@ -32,7 +32,6 @@ export function links() { } export async function loader({ request }) { - console.log({ server: cssModuleStylesheetUrl }); return { enableScripts: new URL(request.url).searchParams.get("disableJs") == null, }; @@ -50,10 +49,6 @@ export default function Root() { window.reactIsHydrated = true; }); - useEffect(() => { - console.log({ client: cssModuleStylesheetUrl }); - }); - let data = useLoaderData(); let matches = useMatches(); From 7449ce30df177e1123a8cc6118baf1158871fad6 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 4 Mar 2022 18:12:04 -0800 Subject: [PATCH 30/71] types --- packages/remix-react/entry.ts | 78 +++++++++++++++++++++++++ packages/remix-server-runtime/entry.ts | 79 ++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) diff --git a/packages/remix-react/entry.ts b/packages/remix-react/entry.ts index ee9491266f0..aabcf126895 100644 --- a/packages/remix-react/entry.ts +++ b/packages/remix-react/entry.ts @@ -36,8 +36,86 @@ interface CssModuleFileContents { css: string; sourceMap: string | null; json: CssModuleClassMap; + moduleExports: CssModuleExports; + dependencies: CssModuleDependency[]; } type CssModuleFileMap = Record; type CssModuleClassMap = Record; + +/** + * Many of these types were copied from @parcel/css and modified slightly for + * our needs. Some of this data will end up in the assets manifest but we don't + * want more than we need. + */ + +type CssModuleExports = { + [name: string]: CssModuleExport; +}; + +interface CssModuleExport { + name: string; + composes: CssModuleReference[]; +} + +type CssModuleReference = + | LocalCssModuleReference + | GlobalCssModuleReference + | DependencyCssModuleReference; + +interface LocalCssModuleReference { + type: "local"; + name: string; +} + +interface GlobalCssModuleReference { + type: "global"; + name: string; +} + +interface DependencyCssModuleReference { + type: "dependency"; + name: string; + /** The dependency specifier for the referenced file. */ + specifier: string; +} + +type CssModuleDependency = CssModuleImportDependency | CssModuleUrlDependency; + +interface CssModuleImportDependency { + type: "import"; + /** The url of the `@import` dependency. */ + url: string; + /** The media query for the `@import` rule. */ + media: string | null; + /** The `supports()` query for the `@import` rule. */ + supports: string | null; + /** The source location where the `@import` rule was found. */ + loc: CssModuleSourceLocation; +} + +interface CssModuleUrlDependency { + type: "url"; + /** The url of the dependency. */ + url: string; + /** The source location where the `url()` was found. */ + loc: CssModuleSourceLocation; + /** The placeholder that the url was replaced with. */ + placeholder: string; +} + +interface CssModuleSourceLocation { + /** The file path in which the dependency exists. */ + filePath: string; + /** The start location of the dependency. */ + start: { + line: number; + column: number; + }; + /** The end location (inclusive) of the dependency. */ + end: { + line: number; + column: number; + }; +} diff --git a/packages/remix-server-runtime/entry.ts b/packages/remix-server-runtime/entry.ts index 996ec7b7c42..4c9d0be1eb7 100644 --- a/packages/remix-server-runtime/entry.ts +++ b/packages/remix-server-runtime/entry.ts @@ -39,7 +39,10 @@ interface CssModulesResults { interface CssModuleFileContents { css: string; + sourceMap: string | null; json: CssModuleClassMap; + moduleExports: CssModuleExports; + dependencies: CssModuleDependency[]; } type CssModuleFileMap = Record; @@ -65,3 +68,79 @@ export function createEntryRouteModules( return memo; }, {} as RouteModules); } + +/** + * Many of these types were copied from @parcel/css and modified slightly for + * our needs. Some of this data will end up in the assets manifest but we don't + * want more than we need. + */ + +type CssModuleExports = { + [name: string]: CssModuleExport; +}; + +interface CssModuleExport { + name: string; + composes: CssModuleReference[]; +} + +type CssModuleReference = + | LocalCssModuleReference + | GlobalCssModuleReference + | DependencyCssModuleReference; + +interface LocalCssModuleReference { + type: "local"; + name: string; +} + +interface GlobalCssModuleReference { + type: "global"; + name: string; +} + +interface DependencyCssModuleReference { + type: "dependency"; + name: string; + /** The dependency specifier for the referenced file. */ + specifier: string; +} + +type CssModuleDependency = CssModuleImportDependency | CssModuleUrlDependency; + +interface CssModuleImportDependency { + type: "import"; + /** The url of the `@import` dependency. */ + url: string; + /** The media query for the `@import` rule. */ + media: string | null; + /** The `supports()` query for the `@import` rule. */ + supports: string | null; + /** The source location where the `@import` rule was found. */ + loc: CssModuleSourceLocation; +} + +interface CssModuleUrlDependency { + type: "url"; + /** The url of the dependency. */ + url: string; + /** The source location where the `url()` was found. */ + loc: CssModuleSourceLocation; + /** The placeholder that the url was replaced with. */ + placeholder: string; +} + +interface CssModuleSourceLocation { + /** The file path in which the dependency exists. */ + filePath: string; + /** The start location of the dependency. */ + start: { + line: number; + column: number; + }; + /** The end location (inclusive) of the dependency. */ + end: { + line: number; + column: number; + }; +} From 0915e6c779e42bfc9720adbedaf91b30b3b23273 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 4 Mar 2022 18:12:47 -0800 Subject: [PATCH 31/71] swap postcss for parcel --- packages/remix-dev/compiler.ts | 17 +- .../remix-dev/compiler/plugins/cssModules.ts | 254 ++++++++---------- packages/remix-dev/package.json | 2 - yarn.lock | 113 +------- 4 files changed, 115 insertions(+), 271 deletions(-) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index a2ffeacfe78..6c9c0666f71 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -23,15 +23,13 @@ import { serverBareModulesPlugin } from "./compiler/plugins/serverBareModulesPlu import { serverEntryModulePlugin } from "./compiler/plugins/serverEntryModulePlugin"; import { serverRouteModulesPlugin } from "./compiler/plugins/serverRouteModulesPlugin"; import type { - CssModuleClassMap, + CssModuleFileContents, CssModulesResults, } from "./compiler/plugins/cssModules"; import { cssModulesPlugin, cssModulesFakerPlugin, getCssModulesFileReferences, - browserCssModulesStylesheetPlugin, - serverCssModulesStylesheetPlugin, } from "./compiler/plugins/cssModules"; import { writeFileSafe } from "./compiler/utils/fs"; @@ -342,24 +340,19 @@ async function createBrowserBuild( options: BuildOptions & { incremental?: boolean } ): Promise { let cssModulesContent = ""; - let cssModulesJson: CssModuleClassMap = {}; - let cssModulesMap: Record< - string, - { css: string; json: CssModuleClassMap; sourceMap: string | null } - > = {}; + let cssModulesMap: Record = {}; function handleProcessedCss( filePath: string, - css: string, - sourceMap: string | null, - json: CssModuleClassMap + { css, dependencies, moduleExports, json }: CssModuleFileContents ) { cssModulesContent += css; - cssModulesJson = { ...cssModulesJson, ...json }; cssModulesMap = { ...cssModulesMap, [filePath]: { css, json, + moduleExports, + dependencies, // TODO: Implement sourcemaps sourceMap: null, }, diff --git a/packages/remix-dev/compiler/plugins/cssModules.ts b/packages/remix-dev/compiler/plugins/cssModules.ts index 4bdea011d65..e0ba68c2ffd 100644 --- a/packages/remix-dev/compiler/plugins/cssModules.ts +++ b/packages/remix-dev/compiler/plugins/cssModules.ts @@ -1,5 +1,3 @@ -import postcss from "postcss"; -import cssModules from "postcss-modules"; import path from "path"; import chalk from "chalk"; import * as fse from "fs-extra"; @@ -13,9 +11,6 @@ import type { AssetsManifestPromiseRef } from "./serverAssetsManifestPlugin"; const suffixMatcher = /\.module\.css?$/; -// TODO: Remove when finished comparing Parcel + PostCSS -const USE_PARCEL = true; - let parcelTransform: (opts: ParcelTransformOptions) => ParcelTransformResult; const decoder = new TextDecoder(); @@ -27,22 +22,18 @@ export function cssModulesPlugin( config: RemixConfig, handleProcessedCss: ( filePath: string, - css: string, - sourceMap: string | null, - json: CssModuleClassMap + { css, sourceMap, json }: CssModuleFileContents ) => void ): esbuild.Plugin { return { name: "css-modules", async setup(build) { build.onResolve({ filter: suffixMatcher }, async (args) => { - if (USE_PARCEL) { - try { - if (!parcelTransform) { - parcelTransform = (await import("@parcel/css")).default.transform; - console.log({ parcelTransform }); - console.warn( - chalk.yellow(` + try { + if (!parcelTransform) { + parcelTransform = (await import("@parcel/css")).default.transform; + console.warn( + chalk.yellow(` -------------------------------------------------------------------------------- CSS Modules support in Remix is experimental. It's implementation may change. @@ -52,18 +43,22 @@ https://github.com/remix-run/remix/issues/new?labels=bug&template=bug_report.yml -------------------------------------------------------------------------------- `) - ); - } - } catch (_) { - throw _; - // throw Error( - // `A CSS Modules file was imported, but the required \`@remix-run/css-modules\` dependency was not found. + ); + } + } catch (_) { + console.error(` +-------------------------------------------------------------------------------- - // Install the dependency by running the following command and restart your app. +A CSS Modules file was imported, but the required \`@remix-run/css-modules\` +dependency was not found. - // npm install @remix-run/css-modules` - // ); - } +Install the dependency by running the following command, then restart your app. + + npm install @remix-run/css-modules + +-------------------------------------------------------------------------------- +`); + throw _; } let path = getResolvedFilePath(config, args); @@ -76,13 +71,10 @@ https://github.com/remix-run/remix/issues/new?labels=bug&template=bug_report.yml build.onLoad({ filter: suffixMatcher }, async (args) => { try { - let { css, json, sourceMap } = await processCssCached( - config, - args.path - ); - handleProcessedCss(args.path, css, sourceMap, json); + let processed = await processCssCached(config, args.path); + handleProcessedCss(args.path, processed); return { - contents: JSON.stringify(json), + contents: JSON.stringify(processed.json), loader: "json", }; } catch (err: any) { @@ -127,36 +119,6 @@ export function cssModulesFakerPlugin( }; } -export function serverCssModulesStylesheetPlugin(): esbuild.Plugin { - let filter = /^@remix-run\/css-modules$/; - return { - name: "server-css-modules-stylesheet", - async setup(build) { - build.onResolve({ filter }, ({ path }) => { - return { - path: path + "/server", - namespace: "server-css-modules-stylesheet", - }; - }); - }, - }; -} - -export function browserCssModulesStylesheetPlugin(): esbuild.Plugin { - let filter = /^@remix-run\/css-modules$/; - return { - name: "browser-css-modules-stylesheet", - async setup(build) { - build.onResolve({ filter }, ({ path }) => { - return { - path: path + "/browser", - namespace: "browser-css-modules-stylesheet", - }; - }); - }, - }; -} - async function processCssCached( config: RemixConfig, filePath: string @@ -167,7 +129,6 @@ async function processCssCached( // Use an on-disk cache to speed up dev server boot. let processedCssPromise = (async function () { let key = file + ".cssmodule"; - let cached: (CssModuleFileContents & { hash: string }) | null = null; try { cached = await cache.getJson(config.cacheDirectory, key); @@ -176,52 +137,21 @@ async function processCssCached( } if (!cached || cached.hash !== hash) { - let { css, json, sourceMap } = await processCss(config, filePath); - cached = { hash, css, json, sourceMap }; - + let processed = await processCss(config, filePath); + cached = { hash, ...processed }; try { await cache.putJson(config.cacheDirectory, key, cached); } catch (error) { // Ignore cache put errors. } } - return cached; })(); return processedCssPromise; } -async function processCssWithPostCss( - file: string -): Promise { - let json: CssModuleClassMap = {}; - let source = await fse.readFile(file, "utf-8"); - let { css, map: mapRaw } = await postcss([ - cssModules({ - localsConvention: "camelCase", - // [name] -> CSS modules file-name (button.module.css -> button-module) - // [local] -> locally assigned classname - // example: - // in button.module.css: .button {} - // generated classname: .button-module__button_wtIDeq {} - generateScopedName: "[name]__[local]_[hash:base64:8]", - hashPrefix: "remix", - getJSON(_, data) { - json = { ...data }; - return json; - }, - }), - ]).process(source, { - from: undefined, - map: true, - }); - - let sourceMap = mapRaw ? mapRaw.toString() : null; - return { css, json, sourceMap }; -} - -async function processCssWithParcel( +async function processCss( config: RemixConfig, file: string ): Promise { @@ -233,18 +163,9 @@ async function processCssWithParcel( code: source, cssModules: true, minify: process.env.NODE_ENV === "production", - // Users will not be able to @import other stylesheets in modules with this - // limitation, nor can you compose classes from outside stylesheets as we'd - // have to decide where and how we want to handle duplicate dependencies in - // various stylesheets. This is not a feature in CSS Modules specifically, - // but other frameworks may support it. We might want to do more research - // here, but in the mean time any dependencies should be imported as a - // separate global stylesheet and loaded before the CSS Modules stylesheet. analyzeDependencies: true, sourceMap: true, - drafts: { - nesting: true, - }, + drafts: { nesting: true }, }); let parcelExports = res.exports || {}; @@ -260,16 +181,13 @@ async function processCssWithParcel( let css = decoder.decode(res.code); let sourceMap = res.map ? decoder.decode(res.map) : null; - return { css, json, sourceMap }; -} - -async function processCss( - config: RemixConfig, - file: string -): Promise { - return await (USE_PARCEL - ? processCssWithParcel(config, file) - : processCssWithPostCss(file)); + return { + css, + json, + sourceMap, + moduleExports: res.exports || {}, + dependencies: res.dependencies || [], + }; } function getResolvedFilePath( @@ -302,10 +220,11 @@ export function getCssModulesFileReferences( * When a user composes classnames in CSS modules, the value returned for the * JSON map is a concat'd version of all the classname strings composed. Note * that the user may compose classnames referenced in other CSS module files, - * but that will require us to juggle dependencies and we're not quite ready for - * that yet. Will revisit that later. + * but that will require us to juggle dependencies which is a little tricky with + * the current build since we don't know for sure if that dependency has been + * processed yet. Coming back to this. */ -function getComposedClassNames(name: string, composes: ParcelComposeData[]) { +function getComposedClassNames(name: string, composes: CssModuleReference[]) { return composes.reduce((prev, cur) => { // skip dependencies for now if (cur.type === "dependency") return prev; @@ -315,8 +234,10 @@ function getComposedClassNames(name: string, composes: ParcelComposeData[]) { export interface CssModuleFileContents { css: string; - json: CssModuleClassMap; sourceMap: string | null; + json: CssModuleClassMap; + moduleExports: CssModuleExports; + dependencies: CssModuleDependency[]; } export type CssModuleFileMap = Record; @@ -329,54 +250,97 @@ export interface CssModulesResults { export type CssModuleClassMap = Record; -// Copy/pasted some types to avoid imports since we're doing that dynamically +// Copy/pasted some types from @parcel/css to avoid dependency issues interface ParcelTransformOptions { filename: string; code: Buffer; minify?: boolean; sourceMap?: boolean; - targets?: ParcelTargets; cssModules?: boolean; drafts?: { [key: string]: boolean }; analyzeDependencies?: boolean; unusedSymbols?: string[]; } -interface ParcelTargets { - android?: number; - chrome?: number; - edge?: number; - firefox?: number; - ie?: number; - ios_saf?: number; - opera?: number; - safari?: number; - samsung?: number; -} - interface ParcelTransformResult { code: Buffer; map: Buffer | void; - exports: ParcelCSSModuleExports | void; - dependencies: any[] | void; + exports: CssModuleExports | void; + dependencies: CssModuleDependency[] | void; } -type ParcelCSSModuleExports = { - [name: string]: ParcelCSSModuleExport; +/** + * Many of these types were copied from @parcel/css and modified slightly for + * our needs. Some of this data will end up in the assets manifest but we don't + * want more than we need. + */ + +type CssModuleExports = { + [name: string]: CssModuleExport; }; -interface ParcelCSSModuleExport { +interface CssModuleExport { + name: string; + composes: CssModuleReference[]; +} + +type CssModuleReference = + | LocalCssModuleReference + | GlobalCssModuleReference + | DependencyCssModuleReference; + +interface LocalCssModuleReference { + type: "local"; name: string; - isReferenced: boolean; - composes: ParcelComposeData[]; } -interface ParcelComposeData { - type: "local" | "global" | "dependency"; +interface GlobalCssModuleReference { + type: "global"; name: string; } -interface ParcelComposeData { - type: "local" | "global" | "dependency"; +interface DependencyCssModuleReference { + type: "dependency"; name: string; + /** The dependency specifier for the referenced file. */ + specifier: string; +} + +type CssModuleDependency = CssModuleImportDependency | CssModuleUrlDependency; + +interface CssModuleImportDependency { + type: "import"; + /** The url of the `@import` dependency. */ + url: string; + /** The media query for the `@import` rule. */ + media: string | null; + /** The `supports()` query for the `@import` rule. */ + supports: string | null; + /** The source location where the `@import` rule was found. */ + loc: CssModuleSourceLocation; +} + +interface CssModuleUrlDependency { + type: "url"; + /** The url of the dependency. */ + url: string; + /** The source location where the `url()` was found. */ + loc: CssModuleSourceLocation; + /** The placeholder that the url was replaced with. */ + placeholder: string; +} + +interface CssModuleSourceLocation { + /** The file path in which the dependency exists. */ + filePath: string; + /** The start location of the dependency. */ + start: { + line: number; + column: number; + }; + /** The end location (inclusive) of the dependency. */ + end: { + line: number; + column: number; + }; } diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index e2eadd0873b..160608bd80e 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -28,8 +28,6 @@ "lodash.debounce": "^4.0.8", "meow": "^7.1.1", "minimatch": "^3.0.4", - "postcss": "^8.4.6", - "postcss-modules": "^4.3.1", "pretty-ms": "^7.0.1", "read-package-json-fast": "^2.0.2", "remark-frontmatter": "^4.0.0", diff --git a/yarn.lock b/yarn.lock index b262502fdb5..3df8cd9b216 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4115,11 +4115,6 @@ css@^3.0.0: source-map "^0.6.1" source-map-resolve "^0.6.0" -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - cssom@^0.4.4: version "0.4.4" resolved "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz" @@ -5485,13 +5480,6 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= -generic-names@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/generic-names/-/generic-names-4.0.0.tgz#0bd8a2fd23fe8ea16cbd0a279acd69c06933d9a3" - integrity sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A== - dependencies: - loader-utils "^3.2.0" - gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" @@ -5866,16 +5854,6 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -icss-replace-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" - integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0= - -icss-utils@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" - integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== - ieee754@1.1.13: version "1.1.13" resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz" @@ -7211,11 +7189,6 @@ loader-utils@^2.0.0: emojis-list "^3.0.0" json5 "^2.1.2" -loader-utils@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.2.0.tgz#bcecc51a7898bee7473d4bc6b845b23af8304d4f" - integrity sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ== - locate-path@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz" @@ -7231,11 +7204,6 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" -lodash.camelcase@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" - integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= - lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" @@ -8072,11 +8040,6 @@ nanocolors@^0.1.0, nanocolors@^0.1.5: resolved "https://registry.npmjs.org/nanocolors/-/nanocolors-0.1.12.tgz" integrity sha512-2nMHqg1x5PU+unxX7PGY7AuYxl2qDx7PSrTRjizr8sxdd3l/3hBuWWaki62qmtYm2U5i4Z5E7GbjlyDFhs9/EQ== -nanoid@^3.2.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" - integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== - nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -8618,70 +8581,6 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= -postcss-modules-extract-imports@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" - integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== - -postcss-modules-local-by-default@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c" - integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ== - dependencies: - icss-utils "^5.0.0" - postcss-selector-parser "^6.0.2" - postcss-value-parser "^4.1.0" - -postcss-modules-scope@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06" - integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== - dependencies: - postcss-selector-parser "^6.0.4" - -postcss-modules-values@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" - integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== - dependencies: - icss-utils "^5.0.0" - -postcss-modules@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/postcss-modules/-/postcss-modules-4.3.1.tgz#517c06c09eab07d133ae0effca2c510abba18048" - integrity sha512-ItUhSUxBBdNamkT3KzIZwYNNRFKmkJrofvC2nWab3CPKhYBQ1f27XXh1PAPE27Psx58jeelPsxWB/+og+KEH0Q== - dependencies: - generic-names "^4.0.0" - icss-replace-symbols "^1.1.0" - lodash.camelcase "^4.3.0" - postcss-modules-extract-imports "^3.0.0" - postcss-modules-local-by-default "^4.0.0" - postcss-modules-scope "^3.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: - version "6.0.9" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.9.tgz#ee71c3b9ff63d9cd130838876c13a2ec1a992b2f" - integrity sha512-UO3SgnZOVTwu4kyLR22UQ1xZh086RyNZppb7lLAKBFK8a32ttG5i87Y/P3+2bRSjZNyJ1B7hfFNo273tKe9YxQ== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - -postcss-value-parser@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" - integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== - -postcss@^8.4.6: - version "8.4.6" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.6.tgz#c5ff3c3c457a23864f32cb45ac9b741498a09ae1" - integrity sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA== - dependencies: - nanoid "^3.2.0" - picocolors "^1.0.0" - source-map-js "^1.0.2" - prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" @@ -9714,11 +9613,6 @@ sort-package-json@^1.54.0: is-plain-obj "2.1.0" sort-object-keys "^1.1.3" -source-map-js@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" - integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== - source-map-resolve@^0.5.0: version "0.5.3" resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" @@ -9859,11 +9753,6 @@ streamsearch@0.1.2: resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz" integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= -string-hash@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" - integrity sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs= - string-length@^4.0.1: version "4.0.2" resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz" @@ -10654,7 +10543,7 @@ use@^3.1.0: resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== -util-deprecate@^1.0.1, util-deprecate@^1.0.2: +util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= From 7a7bfd54a77413d31b785ab7c935d669d809541c Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 4 Mar 2022 18:15:48 -0800 Subject: [PATCH 32/71] cleanup --- fixtures/gists-app/app/root.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fixtures/gists-app/app/root.jsx b/fixtures/gists-app/app/root.jsx index ab86e1706b0..5c61274bcec 100644 --- a/fixtures/gists-app/app/root.jsx +++ b/fixtures/gists-app/app/root.jsx @@ -24,9 +24,11 @@ export function links() { href: normalizeHref, }, { rel: "stylesheet", href: stylesHref }, - { rel: "stylesheet", href: cssModuleStylesheetUrl }, + cssModuleStylesheetUrl && { + rel: "stylesheet", + href: cssModuleStylesheetUrl, + }, { rel: "stylesheet", href: "/resources/theme-css" }, - // cssModuleStyles != null && { rel: "stylesheet", href: cssModuleStyles }, { rel: "shortcut icon", href: favicon }, ].filter(Boolean); } From c12a05f7452a9d581dc5c58c76428a3239db918d Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Tue, 15 Mar 2022 08:14:05 -0700 Subject: [PATCH 33/71] revert deno in tsconfig --- tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 34f1102e7b1..f91f37ff03e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,6 @@ { "path": "packages/remix-cloudflare-pages" }, { "path": "packages/remix-cloudflare-workers" }, { "path": "packages/remix-css-modules" }, - { "path": "packages/remix-deno" }, { "path": "packages/remix-dev" }, { "path": "packages/remix-express" }, { "path": "packages/remix-netlify" }, From c399fe293fa8c0268e4f03926d42a1626df9cf56 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Tue, 15 Mar 2022 13:23:04 -0700 Subject: [PATCH 34/71] add placeholder for tests --- integration/css-modules-test.ts | 225 +++++++++++++++++++++++++ integration/helpers/create-fixture.tsx | 1 + 2 files changed, 226 insertions(+) create mode 100644 integration/css-modules-test.ts diff --git a/integration/css-modules-test.ts b/integration/css-modules-test.ts new file mode 100644 index 00000000000..3dcad2a0c28 --- /dev/null +++ b/integration/css-modules-test.ts @@ -0,0 +1,225 @@ +import { + createAppFixture, + createFixture, + css, + js, + // selectHtml, +} from "./helpers/create-fixture"; +import type { Fixture, AppFixture } from "./helpers/create-fixture"; + +describe("rendering", () => { + let fixture: Fixture; + let app: AppFixture; + + beforeAll(async () => { + fixture = await createFixture({ + files: { + "app/root.jsx": js` + import { Links, Outlet, Scripts } from "remix"; + import stylesHref from "~/styles.css"; + + export function links() { + return [ + { rel: "stylesheet", href: stylesHref }, + ]; + } + + export default function Root() { + return ( + + + + + +
+ +
+ + + + ) + } + `, + + "app/styles.css": css` + .reset--button { + appearance: none; + display: inline-block; + border: none; + padding: 0; + text-decoration: none; + background: 0; + color: inherit; + font: inherit; + } + `, + + "app/routes/index.jsx": js` + import { Badge } from "~/lib/badge"; + export default function() { + return ( +
+

Index

+ Hello +
+ ); + } + `, + + "app/routes/a.jsx": js` + import { Badge } from "~/lib/badge"; + import { Button } from "~/lib/button"; + import { Heading } from "~/lib/heading"; + import { Text } from "~/lib/text"; + export default function() { + return ( +
+ Route A + Welcome + This is really good information, eh? + +
+ ); + } + `, + + "app/lib/button.jsx": js` + import styles from "./button.module.css"; + export function Button(props) { + return ( + + + ); + } + `, + "app/lib/button.jsx": js` import styles from "./button.module.css"; - export function Button(props) { + export function Button({ variant, ...props }) { return ( + ); +}); +Button.displayName = "Button"; +export default Button; diff --git a/fixtures/gists-app/app/components/Button.module.css b/fixtures/gists-app/app/components/Button.module.css new file mode 100644 index 00000000000..8fb41520398 --- /dev/null +++ b/fixtures/gists-app/app/components/Button.module.css @@ -0,0 +1,15 @@ +.button { + appearance: none; + font-size: 1rem; + display: inline-block; + padding: 6px 12px; + text-align: center; + text-decoration: none; + white-space: nowrap; + background: var(--nc-lk-1); + color: var(--nc-lk-tx); + border: 0; + border-radius: 4px; + box-sizing: border-box; + cursor: pointer; +} diff --git a/fixtures/gists-app/app/components/Counter.module.css b/fixtures/gists-app/app/components/Counter.module.css index 349d5bb7d83..d47b277a58e 100644 --- a/fixtures/gists-app/app/components/Counter.module.css +++ b/fixtures/gists-app/app/components/Counter.module.css @@ -1,4 +1,8 @@ .button { + /* + * TODO: support composition from external module file + composes: button from "./Button.module.css"; + */ color: yellow; } diff --git a/fixtures/gists-app/package.json b/fixtures/gists-app/package.json index f4eb2fcf183..ed1a974495b 100644 --- a/fixtures/gists-app/package.json +++ b/fixtures/gists-app/package.json @@ -5,6 +5,7 @@ "scripts": { "postinstall": "node scripts/install_remix.js && node node_modules/@remix-run/dev/cli.js setup", "build": "node node_modules/@remix-run/dev/cli.js build", + "predev": "npm run build", "dev": "pm2-dev pm2.config.js", "start": "node ./build", "routes": "node node_modules/@remix-run/dev/cli.js routes" diff --git a/integration/css-modules-test.ts b/integration/css-modules-test.ts index b17f4dd1e6b..90799d3c209 100644 --- a/integration/css-modules-test.ts +++ b/integration/css-modules-test.ts @@ -4,7 +4,6 @@ import { collectResponses, css, js, - selectHtml, getElement, } from "./helpers/create-fixture"; import type { Fixture, AppFixture } from "./helpers/create-fixture"; @@ -261,7 +260,7 @@ describe("rendering", () => { ).toBe(true); }); - it.todo("composes from locally scoped classname", async () => { + it("composes from locally scoped classname", async () => { let res = await app.page.goto("/b"); let button = getElement(await res.text(), "[data-ui-button]"); let buttonClasses = button.attr("class").split(" "); @@ -281,7 +280,7 @@ describe("rendering", () => { // TODO: Feature not implemented yet // it.todo("composes from imported module classname", () => {}); - it.todo("composes :global selector with :local selector", async () => { + it("composes :global selector with :local selector", async () => { let res = await app.page.goto("/"); let badge = getElement(await res.text(), "[data-ui-badge]"); let buttonClasses = badge.attr("class").split(" "); diff --git a/package.json b/package.json index cece0d84747..2a6f8ef7bd8 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "scripts": { "clean": "git clean -fdX .", "build": "rollup -c && tsc -b", + "predev": "npm install --prefix ./fixtures/gists-app/", "dev": "yarn build && yarn --cwd ./fixtures/gists-app/ dev", "license": "node ./scripts/license.js", "publish": "node ./scripts/publish.js", From 31be49a8c774a6778968b350c240add5b7e9db3d Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Thu, 31 Mar 2022 08:35:20 -0700 Subject: [PATCH 43/71] use composition in test cases --- fixtures/gists-app/app/components/Counter.js | 5 +++-- .../gists-app/app/components/Counter.module.css | 7 +++++-- fixtures/gists-app/app/pages/one.js | 13 +++++++++++++ 3 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 fixtures/gists-app/app/pages/one.js diff --git a/fixtures/gists-app/app/components/Counter.js b/fixtures/gists-app/app/components/Counter.js index ddcbca1f55f..23733dbb522 100644 --- a/fixtures/gists-app/app/components/Counter.js +++ b/fixtures/gists-app/app/components/Counter.js @@ -1,16 +1,17 @@ import { useState } from "react"; +import Button from "./Button"; import styles from "./Counter.module.css"; export default function Counter() { let [count, setCount] = useState(0); return ( - + ); } diff --git a/fixtures/gists-app/app/components/Counter.module.css b/fixtures/gists-app/app/components/Counter.module.css index d47b277a58e..a3b6d7e80f7 100644 --- a/fixtures/gists-app/app/components/Counter.module.css +++ b/fixtures/gists-app/app/components/Counter.module.css @@ -6,7 +6,10 @@ color: yellow; } -.inner { - composes: button; +.bold { font-weight: bold; } + +.inner { + composes: bold; +} diff --git a/fixtures/gists-app/app/pages/one.js b/fixtures/gists-app/app/pages/one.js new file mode 100644 index 00000000000..8b9d5451894 --- /dev/null +++ b/fixtures/gists-app/app/pages/one.js @@ -0,0 +1,13 @@ +import Link from 'next/link'; +import Button from '../styles/button.jsx'; + +export default function PageTest() { + return ( +
+ + Home + + +
+ ); +} From 392e5ca2fd1d8666f5e3988e3c18983217db6fff Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Thu, 31 Mar 2022 10:42:50 -0700 Subject: [PATCH 44/71] fix sorting and caching issues --- packages/remix-dev/compiler.ts | 5 +- .../remix-dev/compiler/plugins/cssModules.ts | 217 +++++++++++------- 2 files changed, 134 insertions(+), 88 deletions(-) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index d5dff3edec8..93457c08c6c 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -341,7 +341,7 @@ async function createBrowserBuild( let cssModulesMap: Record = {}; function handleProcessedCss( filePath: string, - { css, dependencies, moduleExports, json }: CssModuleFileContents + { css, dependencies, moduleExports, json, source }: CssModuleFileContents ) { // For now we are simply keeping processed CSS content and the DIY asset app // in local mutable state. We may want to reconsider this as it will pose @@ -354,12 +354,11 @@ async function createBrowserBuild( cssModulesMap = { ...cssModulesMap, [filePath]: { + source, css, json, moduleExports, dependencies, - // TODO: Implement sourcemaps - sourceMap: null, }, }; } diff --git a/packages/remix-dev/compiler/plugins/cssModules.ts b/packages/remix-dev/compiler/plugins/cssModules.ts index cf3ca8e530d..1af035a3306 100644 --- a/packages/remix-dev/compiler/plugins/cssModules.ts +++ b/packages/remix-dev/compiler/plugins/cssModules.ts @@ -1,15 +1,17 @@ import path from "path"; -import chalk from "chalk"; import * as fse from "fs-extra"; +import chalk from "chalk"; import type * as esbuild from "esbuild"; -import { getFileHash, getHash } from "../utils/crypto"; -import * as cache from "../../cache"; +import { /* getFileHash, */ getHash } from "../utils/crypto"; +// import * as cache from "../../cache"; import type { RemixConfig } from "../../config"; import { resolveUrl } from "../utils/url"; import type { AssetsManifestPromiseRef } from "./serverAssetsManifestPlugin"; -const suffixMatcher = /\.module\.css?$/; +const pluginName = "css-modules"; +const pluginNamespace = `${pluginName}-namespace`; +const suffixMatcher = /\.modules?\.css?$/; let parcelTransform: (opts: ParcelTransformOptions) => ParcelTransformResult; const decoder = new TextDecoder(); @@ -22,11 +24,11 @@ export function cssModulesPlugin( config: RemixConfig, handleProcessedCss: ( filePath: string, - { css, sourceMap, json }: CssModuleFileContents + { css, json }: CssModuleFileContents ) => void ): esbuild.Plugin { return { - name: "css-modules", + name: pluginName, async setup(build) { build.onResolve({ filter: suffixMatcher }, async (args) => { try { @@ -63,26 +65,33 @@ Install the dependency by running the following command, then restart your app. let path = getResolvedFilePath(config, args); return { + pluginName, + namespace: pluginNamespace, path, - namespace: "css-modules", sideEffects: false, }; }); - build.onLoad({ filter: suffixMatcher }, async (args) => { - try { - let processed = await processCssCached(config, args.path); - handleProcessedCss(args.path, processed); - return { - contents: JSON.stringify(processed.json), - loader: "json", - }; - } catch (err: any) { - return { - errors: [{ text: err.message }], - }; + build.onLoad( + { filter: suffixMatcher, namespace: pluginNamespace }, + async (args) => { + try { + let processed = await processCssCached({ + config, + filePath: args.path, + }); + handleProcessedCss(args.path, processed); + return { + contents: JSON.stringify(processed.json), + loader: "json", + }; + } catch (err: any) { + return { + errors: [{ text: err.message }], + }; + } } - }); + ); }, }; } @@ -101,91 +110,118 @@ export function cssModulesFakerPlugin( async setup(build) { build.onResolve({ filter: suffixMatcher }, (args) => { return { + pluginName: pluginName + "-faker", + namespace: pluginNamespace + "-faker", path: getResolvedFilePath(config, args), - namespace: "css-modules-faker", sideEffects: false, }; }); - build.onLoad({ filter: suffixMatcher }, async (args) => { - let res = await assetsManifestPromiseRef.current; - let json = res?.cssModules?.moduleMap[args.path].json || {}; - return { - contents: JSON.stringify(json), - loader: "json", - }; - }); + build.onLoad( + { filter: suffixMatcher, namespace: pluginNamespace + "-faker" }, + async (args) => { + let res = await assetsManifestPromiseRef.current; + let json = res?.cssModules?.moduleMap[args.path].json || {}; + return { + contents: JSON.stringify(json), + loader: "json", + }; + } + ); }, }; } -async function processCssCached( - config: RemixConfig, - filePath: string -): Promise { - let file = path.resolve(config.appDirectory, filePath); - let hash = await getFileHash(file); - - // Use an on-disk cache to speed up dev server boot. - let processedCssPromise = (async function () { - let key = file + ".cssmodule"; - let cached: (CssModuleFileContents & { hash: string }) | null = null; - try { - cached = await cache.getJson(config.cacheDirectory, key); - } catch (error) { - // Ignore cache read errors. - } - - if (!cached || cached.hash !== hash) { - let processed = await processCss(config, filePath); - cached = { hash, ...processed }; - try { - await cache.putJson(config.cacheDirectory, key, cached); - } catch (error) { - // Ignore cache put errors. - } - } - return cached; - })(); - - return processedCssPromise; +async function processCssCached({ + config, + filePath, +}: { + config: RemixConfig; + filePath: string; +}): Promise { + // TODO: Caching as implemented not working as expected in dev mode. Bypass for now. + return await processCss({ config, filePath }); + + // let file = path.resolve(config.appDirectory, filePath); + // let hash = await getFileHash(file); + // // Use an on-disk cache to speed up dev server boot. + // let processedCssPromise = (async function () { + // let key = file + ".cssmodule"; + // let cached: (CssModuleFileContents & { hash: string }) | null = null; + // try { + // cached = await cache.getJson(config.cacheDirectory, key); + // } catch (error) { + // // Ignore cache read errors. + // } + // if (!cached || cached.hash !== hash) { + // let processed = await processCss({ config, filePath }); + // cached = { hash, ...processed }; + // try { + // await cache.putJson(config.cacheDirectory, key, cached); + // } catch (error) { + // // Ignore cache put errors. + // } + // } + // return cached; + // })(); + // return processedCssPromise; } -async function processCss( - config: RemixConfig, - file: string -): Promise { - let json: CssModuleClassMap = {}; - let source = await fse.readFile(file); +async function processCss({ + config, + filePath, +}: { + config: RemixConfig; + filePath: string; +}): Promise { + let classPrefix = + path.basename(filePath, path.extname(filePath)).replace(/\./g, "-") + "__"; + let source = await fse.readFile(filePath); let res = parcelTransform({ - filename: path.relative(config.appDirectory, file), + filename: path.relative(config.appDirectory, filePath), code: source, cssModules: true, minify: process.env.NODE_ENV === "production", - analyzeDependencies: true, + analyzeDependencies: true, // TODO: Maybe? sourceMap: true, drafts: { nesting: true }, }); + let json: CssModuleClassMap = {}; + let cssModulesContent = decoder.decode(res.code); let parcelExports = res.exports || {}; - for (let key in parcelExports) { - let props = parcelExports[key]; - json = { - ...json, - [key]: props.composes.length - ? getComposedClassNames(props.name, props.composes) - : props.name, - }; + + // sort() to keep order consistent in different builds + for (let originClass of Object.keys(parcelExports).sort()) { + let props = parcelExports[originClass]; + let patchedClass = props.name; + let prefixedClassName = getPrefixedClassName(classPrefix, patchedClass); + json[originClass] = props.composes.length + ? getComposedClassNames(classPrefix, prefixedClassName, props.composes) + : prefixedClassName; + cssModulesContent = cssModulesContent.replace( + new RegExp(`\\.${patchedClass}`, "g"), + "." + prefixedClassName + ); + } + + let cssWithSourceMap = cssModulesContent; + if (res.map) { + // TODO: Sourcemaps aren't working as expected because we are inlining the + // map at the end of each module. We need to merge them into a single + // inline sourcemap at the end of the build. We can probably use something + // like https://www.npmjs.com/package/merge-source-maps + // cssWithSourceMap += `\n/*# sourceMappingURL=data:application/json;base64,${res.map.toString( + // "base64" + // )} */`; } - let css = decoder.decode(res.code); - let sourceMap = res.map ? decoder.decode(res.map) : null; return { - css, + css: cssWithSourceMap, + source: source.toString(), json, - sourceMap, - moduleExports: res.exports || {}, + moduleExports: parcelExports, dependencies: res.dependencies || [], }; } @@ -224,17 +260,28 @@ export function getCssModulesFileReferences( * the current build since we don't know for sure if that dependency has been * processed yet. Coming back to this. */ -function getComposedClassNames(name: string, composes: CssModuleReference[]) { - return composes.reduce((prev, cur) => { +function getComposedClassNames( + prefix: string, + name: string, + composes: CssModuleReference[] +) { + return composes.reduce((className, composed) => { // skip dependencies for now - if (cur.type === "dependency") return prev; - return prev + " " + cur.name; + if (composed.type === "dependency") { + console.log({ composed }); + return className; + } + return className + " " + getPrefixedClassName(prefix, composed.name); }, name); } +function getPrefixedClassName(prefix: string, name: string) { + return prefix + name; +} + export interface CssModuleFileContents { css: string; - sourceMap: string | null; + source: string; json: CssModuleClassMap; moduleExports: CssModuleExports; dependencies: CssModuleDependency[]; From 2678ee144162698001ec40180e06bf7164ee745a Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Thu, 28 Apr 2022 12:05:34 -0400 Subject: [PATCH 45/71] chore: remove obsolete dev scripts gists app no longer exists --- package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/package.json b/package.json index 93e1c3c2284..908124597e2 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,6 @@ "scripts": { "clean": "git clean -fdX .", "build": "rollup -c && tsc -b", - "predev": "npm install --prefix ./fixtures/gists-app/", - "dev": "yarn build && yarn --cwd ./fixtures/gists-app/ dev", "license": "node ./scripts/license.js", "publish": "node ./scripts/publish.js", "publish:private": "node ./scripts/publish-private.js", From 43f35c6671ecdaaa549ed840252deb75c9cd3d2a Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Thu, 28 Apr 2022 14:03:23 -0400 Subject: [PATCH 46/71] test(css-modules): rewrite test for playwright --- integration/css-modules-test.ts | 530 +++++++++++++++----------------- 1 file changed, 255 insertions(+), 275 deletions(-) diff --git a/integration/css-modules-test.ts b/integration/css-modules-test.ts index 90799d3c209..80930856415 100644 --- a/integration/css-modules-test.ts +++ b/integration/css-modules-test.ts @@ -1,300 +1,280 @@ +import { test, expect } from "@playwright/test"; + +import { PlaywrightFixture } from "./helpers/playwright-fixture"; +import type { Fixture, AppFixture } from "./helpers/create-fixture"; import { createAppFixture, createFixture, - collectResponses, css, js, - getElement, } from "./helpers/create-fixture"; -import type { Fixture, AppFixture } from "./helpers/create-fixture"; - -describe("rendering", () => { - let fixture: Fixture; - let app: AppFixture; - - beforeAll(async () => { - fixture = await createFixture({ - files: { - "app/root.jsx": js` - import { Links, Outlet, Scripts } from "remix"; - import stylesHref from "~/styles.css"; - import cssModuleStylesheetUrl from "@remix-run/css-modules"; - - export function links() { - return [ - { rel: "stylesheet", href: stylesHref }, - { - rel: "stylesheet", - href: cssModuleStylesheetUrl, - "data-css-modules-link": "", - }, - ]; - } - - export default function Root() { - return ( - - - - - -
- -
- - - - ) - } - `, - - "app/styles.css": css` - .reset--button { - appearance: none; - display: inline-block; - border: none; - padding: 0; - text-decoration: none; - background: 0; - color: inherit; - font: inherit; - } - `, - - "app/routes/index.jsx": js` - import { Badge } from "~/lib/badge"; - export default function() { - return ( -
-

Index

- Hello -
- ); - } - `, - - "app/routes/a.jsx": js` - import { Badge } from "~/lib/badge"; - import { Button } from "~/lib/button"; - import { Heading } from "~/lib/heading"; - import { Text } from "~/lib/text"; - export default function() { - return ( -
- Route A - Welcome - This is really good information, eh? - -
- ); - } - `, - "app/routes/b.jsx": js` - import { Button } from "~/lib/button"; - import { Heading } from "~/lib/heading"; - import { Text } from "~/lib/text"; - export default function() { - return ( -
- Route B - Here's a red button - +let fixture: Fixture; +let appFixture: AppFixture; + +test.beforeAll(async () => { + fixture = await createFixture({ + files: { + "app/root.jsx": js` + import { Links, Outlet, Scripts } from "@remix-run/react"; + import cssModuleStylesheetUrl from "@remix-run/css-modules"; + import stylesHref from "~/styles.css"; + export function links() { + return [ + { rel: "stylesheet", href: stylesHref }, + { + rel: "stylesheet", + href: cssModuleStylesheetUrl, + "data-css-modules-link": "", + }, + ]; + } + export default function Root() { + return ( + + + + + +
+
- ); - } - `, - - "app/lib/button.jsx": js` - import styles from "./button.module.css"; - export function Button({ variant, ...props }) { - return ( - +
+ ); + } + `, + "app/routes/b.jsx": js` + import { Button } from "~/lib/button"; + import { Heading } from "~/lib/heading"; + import { Text } from "~/lib/text"; + export default function() { + return ( +
+ Route B + Here's a red button + +
+ ); + } + `, + "app/lib/button.jsx": js` + import styles from "./button.module.css"; + export function Button({ variant, ...props }) { + return ( + - - ); - } - `, - "app/routes/b.jsx": js` - import { Button } from "~/lib/button"; - import { Heading } from "~/lib/heading"; - import { Text } from "~/lib/text"; - export default function() { - return ( -
- Route B - Here's a red button - -
- ); - } - `, - "app/lib/button.jsx": js` - import styles from "./button.module.css"; - export function Button({ variant, ...props }) { - return ( - + + ); + } + `, + "app/routes/page-b.jsx": js` + import { Button } from "~/lib/button"; + import { Heading } from "~/lib/heading"; + import { Text } from "~/lib/text"; + export default function() { + return ( +
+ Route B + Here's a red button + +
+ ); + } + `, + "app/lib/button.jsx": js` + import styles from "./button.module.css"; + export function Button({ variant, ...props }) { + return ( +