diff --git a/.gitignore b/.gitignore index d78013c7..a92163fa 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ types node_modules storybook-static *.log +styles diff --git a/babel.config.js b/babel.config.js index aef2b2f3..0ab01489 100644 --- a/babel.config.js +++ b/babel.config.js @@ -13,6 +13,7 @@ module.exports = { { libraryName: 'antd', style: false, + libraryDirectory: 'es', }, ], ], diff --git a/package.json b/package.json index f5d3caae..2be94967 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,18 @@ "files": [ "cjs", "esm", + "styles", "types" ], + "sideEffects": [ + "**/*.css" + ], "scripts": { - "build:clean": "rimraf cjs esm types", + "build:clean": "rimraf cjs esm styles types", + "build:styles": "node scripts/antd-styles.cjs", "build:es": "rollup --config --bundleConfigAsCjs", "build:ts": "tsc --declaration --declarationDir types --emitDeclarationOnly", - "build": "yarn run build:clean && yarn run build:es && yarn run build:ts", + "build": "yarn run build:clean && yarn run build:styles && yarn run build:es && yarn run build:ts", "validate": "yarn run lint && yarn run typecheck && yarn run test && yarn run test:storybook", "test": "jest", "test:watch": "jest --watch", diff --git a/rollup.config.js b/rollup.config.js index a2c0ee6d..d92ee555 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,45 +1,97 @@ +import path from 'path'; + import { babel } from '@rollup/plugin-babel'; import commonjs from '@rollup/plugin-commonjs'; import resolve from '@rollup/plugin-node-resolve'; import typescript from '@rollup/plugin-typescript'; import peerDepsExternal from 'rollup-plugin-peer-deps-external'; -import styles from 'rollup-plugin-styles'; // eslint-disable-next-line import/extensions import pkg from './package.json'; +const { + BASE, + hasStyleEntry, + styleClosure, +} = require('./scripts/antd-styles.cjs'); + +const stylesDir = path.resolve('styles'); +const dependencyNames = Object.keys(pkg.dependencies); + +const isDependency = (id) => + /^@babel\/runtime|^rc-picker/.test(id) || + dependencyNames.some((name) => id === name || id.startsWith(`${name}/`)); + +// Imports the stylesheets of the antd components a module uses, so that bundlers only ship those +const antdStyleImports = () => ({ + name: 'antd-style-imports', + resolveId(source) { + return source.startsWith(stylesDir) ? { id: source, external: true } : null; + }, + renderChunk(code, chunk) { + const toStyles = path.posix.relative( + path.posix.dirname(chunk.fileName), + '../styles', + ); + return { + code: code.split(`${stylesDir}/`).join(`${toStyles}/`), + map: null, + }; + }, + transform(code, id) { + if (id.includes('node_modules')) return null; + const used = new Set( + [...code.matchAll(/from ['"]antd\/(?:es|lib)\/([a-z-]+)/g)] + .map((match) => match[1]) + .filter(hasStyleEntry), + ); + if (used.size === 0) return null; + const stylesheets = new Set([BASE]); + used.forEach((name) => + styleClosure(name).forEach((s) => stylesheets.add(s)), + ); + const imports = [...stylesheets].map( + (s) => `import '${stylesDir}/${s}.css';`, + ); + return { code: `${imports.join('\n')}\n${code}`, map: null }; + }, +}); + +const toEs = (id) => id.replace(/^antd\/lib\//, 'antd/es/'); +const toLib = (id) => id.replace(/^antd\/es\//, 'antd/lib/'); + +const output = { + preserveModules: true, + preserveModulesRoot: 'src', + sourcemap: true, + assetFileNames: '[name][extname]', + // npm pack drops every node_modules directory, so bundled dependencies need another folder name + entryFileNames: (chunk) => `${chunk.name.replace(/node_modules/g, 'vendor')}.js`, +}; + export default { input: 'src/index.ts', output: [ { - file: pkg.main, + ...output, + dir: 'cjs', format: 'cjs', - sourcemap: true, // https://rollupjs.org/configuration-options/#output-interop // Necessary in order for jest in consumers to work interop: 'auto', - assetFileNames: '[name][extname]', + exports: 'named', + paths: toLib, }, { - file: pkg.module, + ...output, + dir: 'esm', format: 'esm', - sourcemap: true, - assetFileNames: '[name][extname]', + paths: toEs, }, ], - // this resolves to window in the browser, thus enabling caching in the antd code below - // (!) `this` has been rewritten to `undefined` - // https://rollupjs.org/guide/en/#error-this-is-undefined - // node_modules/antd/es/affix/index.js - // 7: import _typeof from "@babel/runtime/helpers/esm/typeof"; - // 8: - // 9: var __decorate = this && this.__decorate || function (decorators, target, key, desc) { - context: 'this', - - // https://www.npmjs.com/package/@rollup/plugin-babel#babelhelpers - external: [/@babel\/runtime/], + external: isDependency, plugins: [ peerDepsExternal(), @@ -48,17 +100,24 @@ export default { typescript({ exclude: '**/*.(test|stories).(ts|tsx)', }), - styles({ - mode: ['extract', 'index.css'], - // Keeps url('#linearGradient-1') of antd's Empty instead of emitting it as an asset - url: false, - less: { - javascriptEnabled: true, - }, + // The TS plugin leaves `import { X } from 'antd'` alone, this maps it to `antd/es/x` + babel({ + babelHelpers: 'bundled', + extensions: ['.ts', '.tsx'], + include: 'src/**', + babelrc: false, + configFile: false, + plugins: [ + [ + 'import', + { libraryName: 'antd', libraryDirectory: 'es', style: false }, + ], + ], }), babel({ // https://www.npmjs.com/package/@rollup/plugin-babel#babelhelpers babelHelpers: 'runtime', }), + antdStyleImports(), ], }; diff --git a/scripts/antd-styles.cjs b/scripts/antd-styles.cjs new file mode 100644 index 00000000..2349984f --- /dev/null +++ b/scripts/antd-styles.cjs @@ -0,0 +1,97 @@ +const fs = require('fs'); +const path = require('path'); + +const root = path.resolve(__dirname, '..'); +const antdEs = path.join(root, 'node_modules/antd/es'); + +const BASE = 'base'; + +function hasStylesheet(component) { + return fs.existsSync(path.join(antdEs, component, 'style/index.less')); +} + +function componentsWithStyles() { + return fs.readdirSync(antdEs).filter(hasStylesheet); +} + +function hasStyleEntry(component) { + return fs.existsSync(path.join(antdEs, component, 'style/index.js')); +} + +function directDependencies(component) { + const source = fs.readFileSync( + path.join(antdEs, component, 'style/index.js'), + 'utf8', + ); + return [...source.matchAll(/^import '\.\.\/\.\.\/([a-z-]+)\/style';/gm)].map( + (match) => match[1], + ); +} + +/** Dependencies first, so that the cascade matches the order of the full stylesheet. */ +function styleClosure(component) { + const ordered = []; + const visited = new Set(); + const visit = (name) => { + if (visited.has(name) || !hasStyleEntry(name)) return; + visited.add(name); + directDependencies(name).forEach(visit); + if (hasStylesheet(name)) ordered.push(name); + }; + visit(component); + return [BASE, ...ordered]; +} + +function lessEntry(name) { + const target = + name === BASE + ? 'antd/es/style/index.less' + : `antd/es/${name}/style/index.less`; + return [ + "@import 'antd/es/style/themes/compact.less';", + `@import (multiple) '${path.join(root, 'src/antd-variables.less')}';`, + `@import '${path.join(root, 'node_modules', target)}';`, + `@import (multiple) '${path.join(root, 'src/antd-variables.less')}';`, + ].join('\n'); +} + +async function buildStyles(outDir) { + const less = require('less'); + const postcss = require('postcss'); + const cssnano = require('cssnano'); + fs.rmSync(outDir, { recursive: true, force: true }); + fs.mkdirSync(outDir, { recursive: true }); + const sizes = {}; + for (const name of [BASE, ...componentsWithStyles()]) { + const { css } = await less.render(lessEntry(name), { + javascriptEnabled: true, + filename: path.join(root, 'src', `${name}.less`), + paths: [path.join(root, 'node_modules')], + plugins: [], + }); + const minified = await postcss([cssnano({ preset: 'default' })]).process( + css, + { from: undefined }, + ); + fs.writeFileSync(path.join(outDir, `${name}.css`), minified.css); + sizes[name] = minified.css.length; + } + return sizes; +} + +module.exports = { + BASE, + componentsWithStyles, + hasStyleEntry, + styleClosure, + buildStyles, +}; + +if (require.main === module) { + buildStyles(path.join(root, 'styles')).then((sizes) => { + const total = Object.values(sizes).reduce((a, b) => a + b, 0); + console.log( + `built ${Object.keys(sizes).length} stylesheets, ${total} bytes`, + ); + }); +} diff --git a/scripts/limes-build.sh b/scripts/limes-build.sh new file mode 100755 index 00000000..7e891c6c --- /dev/null +++ b/scripts/limes-build.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -Eeuo pipefail +# Tag-like production build of the Limes worktree with Rspack stats written to /tmp/per-module-scripts/stats-