Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/sui-bundler/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ This tool works with zero configuration out the box but you could use some confi

`disableTypeScriptLoader`: Flag to determine if typescript loader should be used. (default: `false`)

`plainCssPackages`: Array of npm package names whose `.css` files should bypass `@s-ui/sass-loader` and `postcss-loader`. Use this for packages that ship pre-compiled CSS (e.g. Tailwind CSS v4 output) that would cause Sass parse errors. (default: `[]`)

`cssInAppStyles`: Array of npm package names whose CSS modules should be forced into the `AppStyles` chunk. This prevents webpack's `splitChunks` from extracting them into separate async chunks that `@s-ui/ssr` doesn't know about — avoiding Flash of Unstyled Content. (default: `[]`)

```json
{
"config": {
Expand All @@ -202,6 +206,8 @@ This tool works with zero configuration out the box but you could use some confi
"dev": "cheap-module-eval-source-map",
"prod": "hidden-source-map"
},
"plainCssPackages": ["@adv-mt/ui", "@adv-mt/theme"],
"cssInAppStyles": ["@adv-mt/ui", "@adv-mt/theme"],
"disableTypeScriptLoader": true // Only if you want to disable typescript loader
}
}
Expand Down
21 changes: 20 additions & 1 deletion packages/sui-bundler/shared/module-rules-sass.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,17 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin')

const {cleanList, config, when, isTailwindEnabled} = require('./index')

module.exports = {
const plainCssPackages = config.plainCssPackages || []

const plainCssExclude = plainCssPackages.length
? new RegExp(
`node_modules[\\\\/](${plainCssPackages.map(p => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})[\\\\/]`
)
: null

const sassRule = {
test: /(\.css|\.scss)$/,
...(plainCssExclude ? {exclude: plainCssExclude} : {}),
use: cleanList([
MiniCssExtractPlugin.loader,
require.resolve('css-loader'),
Expand All @@ -29,3 +38,13 @@ module.exports = {
require.resolve('@s-ui/sass-loader')
])
}

const plainCssRule = plainCssExclude
? {
test: /\.css$/,
include: plainCssExclude,
use: [MiniCssExtractPlugin.loader, require.resolve('css-loader')]
}
: null

module.exports = plainCssRule ? [sassRule, plainCssRule] : sassRule
49 changes: 49 additions & 0 deletions packages/sui-bundler/shared/plain-css-split-chunks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const {config} = require('./index')

const cssInAppStyles = config.cssInAppStyles || []

exports.plainCssSplitChunks = () => {
if (!cssInAppStyles.length) return {}

return {
plugins: [
{
apply(compiler) {
const pkgPattern = cssInAppStyles.map(p => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')
const pkgRegex = new RegExp(`[\\\\/]node_modules[\\\\/](${pkgPattern})[\\\\/]`)

compiler.hooks.thisCompilation.tap('CssInAppStyles', compilation => {
compilation.hooks.afterOptimizeChunks.tap('CssInAppStyles', chunks => {
let appStylesChunk = null
for (const chunk of chunks) {
if (chunk.name === 'AppStyles') {
appStylesChunk = chunk
break
}
}
if (!appStylesChunk) return

for (const chunk of chunks) {
if (chunk === appStylesChunk) continue
const modulesToMove = []
for (const module of compilation.chunkGraph.getChunkModulesIterable(chunk)) {
if (module.type !== 'css/mini-extract') continue
const name = module.nameForCondition && module.nameForCondition()
if (name && pkgRegex.test(name)) {
modulesToMove.push(module)
}
}
for (const module of modulesToMove) {
compilation.chunkGraph.disconnectChunkAndModule(chunk, module)
if (!compilation.chunkGraph.isModuleInChunk(module, appStylesChunk)) {
compilation.chunkGraph.connectChunkAndModule(appStylesChunk, module)
}
}
}
})
})
}
}
]
}
}
74 changes: 74 additions & 0 deletions packages/sui-bundler/test/server/plainCssSplitChunksSpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
const {expect} = require('chai')
const path = require('path')

const CONFIG_PATH = require.resolve('../../shared/config.js')
const INDEX_PATH = require.resolve('../../shared/index.js')
const MODULE_PATH = require.resolve('../../shared/plain-css-split-chunks.js')

function loadWithConfig(cssInAppStyles = []) {
delete require.cache[CONFIG_PATH]
delete require.cache[INDEX_PATH]
delete require.cache[MODULE_PATH]

require.cache[CONFIG_PATH] = {
id: CONFIG_PATH,
filename: CONFIG_PATH,
loaded: true,
exports: {
config: {cssInAppStyles},
extractComments: false,
sourceMap: false,
supportLegacyBrowsers: true,
cacheDirectory: path.resolve(process.cwd(), '.sui/cache')
}
}

return require('../../shared/plain-css-split-chunks.js')
}

describe('plainCssSplitChunks', () => {
afterEach(() => {
delete require.cache[CONFIG_PATH]
delete require.cache[INDEX_PATH]
delete require.cache[MODULE_PATH]
})

it('returns empty object when cssInAppStyles is not configured', () => {
const {plainCssSplitChunks} = loadWithConfig([])
expect(plainCssSplitChunks()).to.deep.equal({})
})

it('returns plugins array with one plugin when cssInAppStyles has packages', () => {
const {plainCssSplitChunks} = loadWithConfig(['@adv-mt/ui', '@adv-mt/theme'])
const result = plainCssSplitChunks()

expect(result).to.have.property('plugins')
expect(result.plugins).to.be.an('array').with.lengthOf(1)
})

it('plugin has an apply method', () => {
const {plainCssSplitChunks} = loadWithConfig(['@adv-mt/ui'])
const result = plainCssSplitChunks()

expect(result.plugins[0]).to.have.property('apply')
expect(result.plugins[0].apply).to.be.a('function')
})

it('plugin taps into thisCompilation hook', () => {
const {plainCssSplitChunks} = loadWithConfig(['@adv-mt/ui'])
const result = plainCssSplitChunks()

const taps = []
const fakeCompiler = {
hooks: {
thisCompilation: {
tap: (name, fn) => taps.push({name, fn})
}
}
}

result.plugins[0].apply(fakeCompiler)
expect(taps).to.have.lengthOf(1)
expect(taps[0].name).to.equal('CssInAppStyles')
})
})
31 changes: 3 additions & 28 deletions packages/sui-bundler/webpack.config.client.dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const HtmlWebpackInjectAttributesPlugin = require('html-webpack-inject-attributes-plugin')
const {withHydrationOverlayWebpack} = require('@builder.io/react-hydration-overlay/webpack')

const {envVars, MAIN_ENTRY_POINT, config, cleanList, when, isTailwindEnabled} = require('./shared/index.js')
const {envVars, MAIN_ENTRY_POINT, config, cleanList, when} = require('./shared/index.js')
const definePlugin = require('./shared/define.js')
const manifestLoaderRules = require('./shared/module-rules-manifest-loader.js')
const sassRules = require('./shared/module-rules-sass.js')
const createSVGSpritemapPlugin = require('./shared/svg-spritemap')
const {aliasFromConfig, defaultAlias} = require('./shared/resolve-alias.js')
const {supportLegacyBrowsers, cacheDirectory} = require('./shared/config.js')
Expand Down Expand Up @@ -96,33 +97,7 @@ const webpackConfig = {
module: {
rules: cleanList([
createCompilerRules({supportLegacyBrowsers, isDevelopment: true}),
{
test: /(\.css|\.scss)$/,
use: cleanList([
MiniCssExtractPlugin.loader,
require.resolve('css-loader'),
when(config['externals-manifest'], () => ({
loader: 'externals-manifest-loader',
options: {
manifestURL: config['externals-manifest']
}
})),
{
loader: require.resolve('postcss-loader'),
options: {
postcssOptions: {
plugins: [
...(isTailwindEnabled() ? [require('tailwindcss')()] : []),
require('autoprefixer')({
overrideBrowserslist: config.targets
})
]
}
}
},
require.resolve('@s-ui/sass-loader')
])
},
...(Array.isArray(sassRules) ? sassRules : [sassRules]),
when(config['externals-manifest'], () => manifestLoaderRules(config['externals-manifest']))
])
},
Expand Down
17 changes: 17 additions & 0 deletions packages/sui-bundler/webpack.config.dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ const {supportLegacyBrowsers, cacheDirectory} = require('./shared/config.js')
const {resolveLoader} = require('./shared/resolve-loader.js')
const createCompilerRules = require('./shared/module-rules-compiler.js')

const plainCssPackages = config.plainCssPackages || []
const plainCssExclude = plainCssPackages.length
? new RegExp(
`node_modules[\\\\/](${plainCssPackages.map(p => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})[\\\\/]`
)
: null

const outputPath = path.join(process.cwd(), 'dist')

const {CI = false} = process.env
Expand Down Expand Up @@ -90,6 +97,7 @@ const webpackConfig = {
createCompilerRules({supportLegacyBrowsers, isDevelopment: true}),
{
test: /(\.css|\.scss)$/,
...(plainCssExclude ? {exclude: plainCssExclude} : {}),
use: cleanList([
require.resolve('style-loader'),
when(config['externals-manifest'], () => ({
Expand All @@ -115,6 +123,15 @@ const webpackConfig = {
require.resolve('@s-ui/sass-loader')
])
},
...(plainCssExclude
? [
{
test: /\.css$/,
include: plainCssExclude,
use: [require.resolve('style-loader'), require.resolve('css-loader')]
}
]
: []),
when(config['externals-manifest'], () => manifestLoaderRules(config['externals-manifest']))
])
},
Expand Down
2 changes: 1 addition & 1 deletion packages/sui-bundler/webpack.config.lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ module.exports = ({chunkCss} = {}) => {
definePlugin()
]),
module: {
rules: [createCompilerRules({supportLegacyBrowsers}), sassRules]
rules: [createCompilerRules({supportLegacyBrowsers}), ...(Array.isArray(sassRules) ? sassRules : [sassRules])]
}
}
}
4 changes: 3 additions & 1 deletion packages/sui-bundler/webpack.config.prod.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const InlineChunkHtmlPlugin = require('./shared/inline-chunk-html-plugin.js')

const {when, cleanList, envVars, MAIN_ENTRY_POINT, config} = require('./shared/index.js')
const {plainCssSplitChunks} = require('./shared/plain-css-split-chunks.js')
const {aliasFromConfig} = require('./shared/resolve-alias.js')
const {extractComments, sourceMap, supportLegacyBrowsers, cacheDirectory} = require('./shared/config.js')
const {resolveLoader} = require('./shared/resolve-loader.js')
Expand Down Expand Up @@ -73,6 +74,7 @@ const webpackConfig = {
compression: false
},
plugins: cleanList([
...(plainCssSplitChunks().plugins || []),
new webpack.ProvidePlugin({
process: 'process/browser.js'
}),
Expand Down Expand Up @@ -106,7 +108,7 @@ const webpackConfig = {
module: {
rules: cleanList([
createCompilerRules({supportLegacyBrowsers}),
sassRules,
...(Array.isArray(sassRules) ? sassRules : [sassRules]),
when(config['externals-manifest'], () => manifestLoaderRules(config['externals-manifest']))
])
},
Expand Down
Loading