From 482b4d6ab815350d5f5b71d686bf76cb3d234686 Mon Sep 17 00:00:00 2001 From: Josh Black Date: Thu, 1 Aug 2024 13:31:51 -0500 Subject: [PATCH] feat(react): enable support for CSS Modules and emitting *.css files in bundle (#4800) * fix(import-css): update plugin to work with different output dirs * feat(react): enable support for CSS Modules and emitting *.css files in bundle * chore: update ordering for workspaces * Update dry-trainers-protect.md * Update package.json * refactor: update primer/react to use postcss-preset-primer * chore: update postcss config to mjs * chore: restore original postcss config --------- Co-authored-by: Josh Black --- .changeset/config.json | 2 +- .changeset/dry-trainers-protect.md | 5 + .github/workflows/consumer_test.yml | 2 +- package-lock.json | 63 ++++++++++ package.json | 3 +- packages/react/package.json | 8 +- .../{rollup.config.js => rollup.config.mjs} | 111 ++++++++++++++++-- packages/react/script/build | 2 +- packages/react/script/get-export-sizes.js | 11 ++ packages/react/src/__tests__/exports.test.ts | 2 +- .../rollup-plugin-import-css/src/index.ts | 17 ++- 11 files changed, 208 insertions(+), 18 deletions(-) create mode 100644 .changeset/dry-trainers-protect.md rename packages/react/{rollup.config.js => rollup.config.mjs} (70%) diff --git a/.changeset/config.json b/.changeset/config.json index af251e02462..d826f57cc65 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -6,5 +6,5 @@ "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": ["docs", "example-*", "codesandbox", "rollup-plugin-import-css"] + "ignore": ["docs", "example-*", "codesandbox"] } diff --git a/.changeset/dry-trainers-protect.md b/.changeset/dry-trainers-protect.md new file mode 100644 index 00000000000..fce26ab0815 --- /dev/null +++ b/.changeset/dry-trainers-protect.md @@ -0,0 +1,5 @@ +--- +'@primer/react': major +--- + +Update Primer React to emit *.css files that are imported by emitted *.js files for styling diff --git a/.github/workflows/consumer_test.yml b/.github/workflows/consumer_test.yml index 63973ded26d..52276a5257a 100644 --- a/.github/workflows/consumer_test.yml +++ b/.github/workflows/consumer_test.yml @@ -22,7 +22,7 @@ jobs: - name: Install dependencies run: npm ci - name: Build @primer/react - run: npm run build -w @primer/react + run: npm run build -w rollup-plugin-import-css -w @primer/react # Output the artifact as a tarball in `consumer-test`. Write the # information for this package in `consumer-test/pack.json` so we can read # from it later to install the package diff --git a/package-lock.json b/package-lock.json index cbb820a2cf0..44f9e461428 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "primer", "license": "MIT", "workspaces": [ + "packages/rollup-plugin-import-css", "packages/*", "docs", "examples/*" @@ -37230,6 +37231,19 @@ "version": "1.2.4", "license": "MIT" }, + "node_modules/is-url-superb": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-url-superb/-/is-url-superb-4.0.0.tgz", + "integrity": "sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-valid-domain": { "version": "0.1.6", "license": "MIT", @@ -48747,6 +48761,20 @@ "postcss": "^8.4" } }, + "node_modules/postcss-custom-properties-fallback": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/postcss-custom-properties-fallback/-/postcss-custom-properties-fallback-1.0.2.tgz", + "integrity": "sha512-UrPr99bo03c1iX4iqjBBYo3W+EsXfxrozp2LNvRN34Y95n/7R2RupcMhGlc+C/RQxknDXiP+bptyhmb8nFYzeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "postcss-values-parser": "^6.0.2" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, "node_modules/postcss-custom-selectors": { "version": "7.1.12", "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-7.1.12.tgz", @@ -49807,6 +49835,31 @@ "version": "4.2.0", "license": "MIT" }, + "node_modules/postcss-values-parser": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-6.0.2.tgz", + "integrity": "sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "color-name": "^1.1.4", + "is-url-superb": "^4.0.0", + "quote-unquote": "^1.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "postcss": "^8.2.9" + } + }, + "node_modules/postcss-values-parser/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/prebuild-install": { "version": "7.1.2", "license": "MIT", @@ -50370,6 +50423,13 @@ "node": ">=8" } }, + "node_modules/quote-unquote": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/quote-unquote/-/quote-unquote-1.0.0.tgz", + "integrity": "sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==", + "dev": true, + "license": "MIT" + }, "node_modules/ramda": { "version": "0.29.0", "dev": true, @@ -61053,7 +61113,9 @@ "mdast-util-to-string": "4.0.0", "micromark-extension-frontmatter": "2.0.0", "micromark-extension-mdxjs": "3.0.0", + "postcss-custom-properties-fallback": "1.0.2", "postcss-preset-env": "9.5.14", + "postcss-preset-primer": "^0.0.0", "react": "18.3.1", "react-dnd": "14.0.4", "react-dnd-html5-backend": "14.0.2", @@ -61062,6 +61124,7 @@ "recast": "0.23.7", "rimraf": "5.0.5", "rollup": "4.9.6", + "rollup-plugin-import-css": "^0.0.0", "rollup-plugin-postcss": "4.0.2", "rollup-plugin-visualizer": "5.9.2", "semver": "7.6.2", diff --git a/package.json b/package.json index 43c16d88183..41810e7ef9a 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,14 @@ "name": "primer", "private": true, "workspaces": [ + "packages/rollup-plugin-import-css", "packages/*", "docs", "examples/*" ], "scripts": { "setup": "./script/setup", - "build": "npm run build -w @primer/react", + "build": "npm run build -w rollup-plugin-import-css -w @primer/react", "clean": "npm run clean -ws --if-present", "clean:all": "npm run clean && rimraf node_modules docs/node_modules packages/*/node_modules examples/*/node_modules", "format": "prettier --cache --write '**/*.{js,css,md,mdx,ts,tsx,yml}'", diff --git a/packages/react/package.json b/packages/react/package.json index 8300d4dc9b2..21425e3be81 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -38,7 +38,10 @@ "./lib-esm/internal/*": null }, "typings": "lib/index.d.ts", - "sideEffects": false, + "sideEffects": [ + "lib-esm/**/*.css", + "lib/**/*.css" + ], "scripts": { "build": "./script/build", "clean": "rimraf dist lib lib-esm css", @@ -199,7 +202,9 @@ "mdast-util-to-string": "4.0.0", "micromark-extension-frontmatter": "2.0.0", "micromark-extension-mdxjs": "3.0.0", + "postcss-custom-properties-fallback": "1.0.2", "postcss-preset-env": "9.5.14", + "postcss-preset-primer": "^0.0.0", "react": "18.3.1", "react-dnd": "14.0.4", "react-dnd-html5-backend": "14.0.2", @@ -208,6 +213,7 @@ "recast": "0.23.7", "rimraf": "5.0.5", "rollup": "4.9.6", + "rollup-plugin-import-css": "^0.0.0", "rollup-plugin-postcss": "4.0.2", "rollup-plugin-visualizer": "5.9.2", "semver": "7.6.2", diff --git a/packages/react/rollup.config.js b/packages/react/rollup.config.mjs similarity index 70% rename from packages/react/rollup.config.js rename to packages/react/rollup.config.mjs index 1ef5c35e8fb..099d1e8453f 100644 --- a/packages/react/rollup.config.js +++ b/packages/react/rollup.config.mjs @@ -1,13 +1,21 @@ +import fs from 'node:fs' +import path from 'node:path' +import {fileURLToPath} from 'node:url' import commonjs from '@rollup/plugin-commonjs' import resolve from '@rollup/plugin-node-resolve' import babel from '@rollup/plugin-babel' import replace from '@rollup/plugin-replace' import terser from '@rollup/plugin-terser' import glob from 'fast-glob' +import customPropertiesFallback from 'postcss-custom-properties-fallback' import {visualizer} from 'rollup-plugin-visualizer' +import {importCSS} from 'rollup-plugin-import-css' import postcss from 'rollup-plugin-postcss' +import postssPresetPrimer from 'postcss-preset-primer' import MagicString from 'magic-string' -import packageJson from './package.json' +import packageJson from './package.json' assert {type: 'json'} + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) const input = new Set([ // "exports" @@ -80,6 +88,50 @@ function createPackageRegex(name) { return new RegExp(`^${name}(/.*)?`) } +const postcssPlugins = [ + postssPresetPrimer(), + customPropertiesFallback({ + importFrom: [ + () => { + let customProperties = {} + const filePaths = glob.sync(['fallbacks/**/*.json', 'docs/functional/themes/light.json'], { + cwd: path.join(__dirname, '../../node_modules/@primer/primitives/dist/'), + ignore: ['fallbacks/color-fallbacks.json'], + }) + + for (const filePath of filePaths) { + const fileData = fs.readFileSync( + path.join(__dirname, '../../node_modules/@primer/primitives/dist/', filePath), + 'utf8', + ) + + const jsonData = JSON.parse(fileData) + let result = {} + + if (filePath === 'docs/functional/themes/light.json') { + for (const variable of Object.keys(jsonData)) { + result[`--${variable}`] = jsonData[variable].value + } + } else { + result = jsonData + } + + customProperties = { + ...customProperties, + ...result, + } + } + + return {customProperties} + }, + ], + }), +] + +const postcssModulesOptions = { + generateScopedName: 'prc-[folder]-[local]-[hash:base64:5]', +} + const baseConfig = { input: Array.from(input), plugins: [ @@ -121,12 +173,12 @@ const baseConfig = { commonjs({ extensions, }), - postcss({ - extract: 'components.css', - autoModules: false, - modules: {generateScopedName: 'prc_[local]_[hash:base64:5]'}, - // plugins are defined in postcss.config.js + importCSS({ + modulesRoot: 'src', + postcssPlugins, + postcssModulesOptions, }), + /** * This custom rollup plugin allows us to preserve directives in source * code, such as "use client", in order to support React Server Components. @@ -269,7 +321,52 @@ export default [ 'process.env.NODE_ENV': JSON.stringify('production'), preventAssignment: true, }), - ...baseConfig.plugins, + babel({ + extensions, + exclude: /node_modules/, + babelHelpers: 'inline', + babelrc: false, + configFile: false, + presets: [ + '@babel/preset-typescript', + [ + '@babel/preset-react', + { + modules: false, + }, + ], + ], + plugins: [ + 'macros', + 'add-react-displayname', + 'dev-expression', + 'babel-plugin-styled-components', + '@babel/plugin-proposal-nullish-coalescing-operator', + '@babel/plugin-proposal-optional-chaining', + [ + 'babel-plugin-transform-replace-expressions', + { + replace: { + __DEV__: "process.env.NODE_ENV !== 'production'", + }, + }, + ], + ], + }), + resolve({ + extensions, + }), + commonjs({ + extensions, + }), + // PostCSS plugins are defined in postcss.config.js + postcss({ + extract: 'components.css', + autoModules: false, + modules: { + generateScopedName: 'prc_[local]_[hash:base64:5]', + }, + }), terser(), visualizer({sourcemap: true}), ], diff --git a/packages/react/script/build b/packages/react/script/build index e8117429a3b..e6d2df9d1fe 100755 --- a/packages/react/script/build +++ b/packages/react/script/build @@ -9,7 +9,7 @@ npm run clean npm run build:precompile-color-schemes # Bundle -npx rollup -c --bundleConfigAsCjs +npx rollup -c # Type check npx tsc --project tsconfig.build.json diff --git a/packages/react/script/get-export-sizes.js b/packages/react/script/get-export-sizes.js index dd9f371fa49..d51a2735615 100644 --- a/packages/react/script/get-export-sizes.js +++ b/packages/react/script/get-export-sizes.js @@ -63,6 +63,17 @@ async function main() { commonjs({ include: /node_modules/, }), + { + name: 'empty-css-modules', + transform(_code, id) { + if (!id.endsWith('.css')) { + return + } + return { + code: `export default {}`, + } + }, + }, virtual({ __entrypoint__: `export { ${identifier} } from '${filepath}';`, }), diff --git a/packages/react/src/__tests__/exports.test.ts b/packages/react/src/__tests__/exports.test.ts index 3fc912b6df6..9834a2cd8b8 100644 --- a/packages/react/src/__tests__/exports.test.ts +++ b/packages/react/src/__tests__/exports.test.ts @@ -164,7 +164,7 @@ async function setup(): Promise { } const extension = path.extname(source) - if (extension !== '' && extensions.includes(extension)) { + if (extension !== '' && !extensions.includes(extension)) { return null } diff --git a/packages/rollup-plugin-import-css/src/index.ts b/packages/rollup-plugin-import-css/src/index.ts index dcbde95af76..a4f51a3df3f 100644 --- a/packages/rollup-plugin-import-css/src/index.ts +++ b/packages/rollup-plugin-import-css/src/index.ts @@ -35,11 +35,10 @@ export function importCSS(options: ImportCSSOptions): Plugin { return } - // If we're working with a css asset that is not a CSS module, assume that - // it has been generated by our plugin and should be marked as external. - if (source.endsWith('.css') && !source.endsWith('.module.css')) { + const moduleInfo = this.getModuleInfo(importer) + if (moduleInfo?.meta['import-css']?.source === source) { return { - id: path.resolve(path.dirname(importer), source), + id: source, external: true, } } @@ -87,9 +86,17 @@ export function importCSS(options: ImportCSSOptions): Plugin { fileName, }) + const moduleInfo = this.getModuleInfo(id) + const cssSource = `./${path.basename(fileName)}` + if (moduleInfo) { + moduleInfo.meta['import-css'] = { + source: cssSource, + } + } + return { code: ` - import './${path.basename(fileName)}'; + import '${cssSource}'; export default ${JSON.stringify(cssModuleClasses)} `, }