mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-21 21:25:11 +00:00
refactor(schema,vite,webpack): rework postcss
module loading (#27946)
Co-authored-by: Daniel Roe <daniel@roe.dev>
This commit is contained in:
parent
e717937c03
commit
373d015ae7
@ -62,9 +62,11 @@
|
||||
"@vitejs/plugin-vue": "5.0.5",
|
||||
"@vitest/coverage-v8": "1.6.0",
|
||||
"@vue/test-utils": "2.4.6",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"case-police": "0.6.1",
|
||||
"changelogen": "0.5.5",
|
||||
"consola": "3.2.3",
|
||||
"cssnano": "^7.0.3",
|
||||
"devalue": "5.0.0",
|
||||
"eslint": "9.6.0",
|
||||
"eslint-plugin-no-only-tests": "3.1.0",
|
||||
|
@ -23,6 +23,8 @@ export default defineBuildConfig({
|
||||
externals: [
|
||||
// Type imports
|
||||
'#app/components/nuxt-link',
|
||||
'cssnano',
|
||||
'autoprefixer',
|
||||
'ofetch',
|
||||
'vue-router',
|
||||
'@nuxt/telemetry',
|
||||
|
@ -1,12 +1,45 @@
|
||||
import { defineUntypedSchema } from 'untyped'
|
||||
|
||||
const ensureItemIsLast = (item: string) => (arr: string[]) => {
|
||||
const index = arr.indexOf(item)
|
||||
if (index !== -1) {
|
||||
arr.splice(index, 1)
|
||||
arr.push(item)
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
const orderPresets = {
|
||||
cssnanoLast: ensureItemIsLast('cssnano'),
|
||||
autoprefixerLast: ensureItemIsLast('autoprefixer'),
|
||||
autoprefixerAndCssnanoLast (names: string[]) {
|
||||
return orderPresets.cssnanoLast(orderPresets.autoprefixerLast(names))
|
||||
},
|
||||
}
|
||||
|
||||
export default defineUntypedSchema({
|
||||
postcss: {
|
||||
/**
|
||||
* A strategy for ordering PostCSS plugins.
|
||||
*
|
||||
* @type {'cssnanoLast' | 'autoprefixerLast' | 'autoprefixerAndCssnanoLast' | string[] | ((names: string[]) => string[])}
|
||||
*/
|
||||
order: {
|
||||
$resolve: (val: string | string[] | ((plugins: string[]) => string[])): string[] | ((plugins: string[]) => string[]) => {
|
||||
if (typeof val === 'string') {
|
||||
if (!(val in orderPresets)) {
|
||||
throw new Error(`[nuxt] Unknown PostCSS order preset: ${val}`)
|
||||
}
|
||||
return orderPresets[val as keyof typeof orderPresets]
|
||||
}
|
||||
return val ?? orderPresets.autoprefixerAndCssnanoLast
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Options for configuring PostCSS plugins.
|
||||
*
|
||||
* https://postcss.org/
|
||||
* @type {Record<string, any> & { autoprefixer?: any; cssnano?: any }}
|
||||
* @type {Record<string, Record<string, unknown> | false> & { autoprefixer?: typeof import('autoprefixer').Options; cssnano?: typeof import('cssnano').Options }}
|
||||
*/
|
||||
plugins: {
|
||||
/**
|
||||
|
@ -75,9 +75,10 @@ export interface NuxtBuilder {
|
||||
}
|
||||
|
||||
// Normalized Nuxt options available as `nuxt.options.*`
|
||||
export interface NuxtOptions extends Omit<ConfigSchema, 'builder' | 'webpack'> {
|
||||
export interface NuxtOptions extends Omit<ConfigSchema, 'builder' | 'webpack' | 'postcss'> {
|
||||
sourcemap: Required<Exclude<ConfigSchema['sourcemap'], boolean>>
|
||||
builder: '@nuxt/vite-builder' | '@nuxt/webpack-builder' | NuxtBuilder
|
||||
postcss: Omit<ConfigSchema['postcss'], 'order'> & { order: Exclude<ConfigSchema['postcss']['order'], string> }
|
||||
webpack: ConfigSchema['webpack'] & {
|
||||
$client: ConfigSchema['webpack']
|
||||
$server: ConfigSchema['webpack']
|
||||
|
@ -1,11 +1,16 @@
|
||||
import { requireModule } from '@nuxt/kit'
|
||||
import type { Nuxt } from '@nuxt/schema'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { requireModule, tryResolveModule } from '@nuxt/kit'
|
||||
import type { Nuxt, NuxtOptions } from '@nuxt/schema'
|
||||
import type { InlineConfig as ViteConfig } from 'vite'
|
||||
import { distDir } from './dirs'
|
||||
import { interopDefault } from 'mlly'
|
||||
import type { Plugin } from 'postcss'
|
||||
|
||||
const lastPlugins = ['autoprefixer', 'cssnano']
|
||||
function sortPlugins ({ plugins, order }: NuxtOptions['postcss']): string[] {
|
||||
const names = Object.keys(plugins)
|
||||
return typeof order === 'function' ? order(names) : (order || names)
|
||||
}
|
||||
|
||||
export function resolveCSSOptions (nuxt: Nuxt): ViteConfig['css'] {
|
||||
export async function resolveCSSOptions (nuxt: Nuxt): Promise<ViteConfig['css']> {
|
||||
const css: ViteConfig['css'] & { postcss: NonNullable<Exclude<NonNullable<ViteConfig['css']>['postcss'], string>> } = {
|
||||
postcss: {
|
||||
plugins: [],
|
||||
@ -14,19 +19,26 @@ export function resolveCSSOptions (nuxt: Nuxt): ViteConfig['css'] {
|
||||
|
||||
css.postcss.plugins = []
|
||||
|
||||
const plugins = Object.entries(nuxt.options.postcss.plugins)
|
||||
.sort((a, b) => lastPlugins.indexOf(a[0]) - lastPlugins.indexOf(b[0]))
|
||||
const postcssOptions = nuxt.options.postcss
|
||||
|
||||
for (const [name, opts] of plugins) {
|
||||
if (opts) {
|
||||
// TODO: remove use of requireModule in favour of ESM import
|
||||
const plugin = requireModule(name, {
|
||||
paths: [
|
||||
...nuxt.options.modulesDir,
|
||||
distDir,
|
||||
],
|
||||
})
|
||||
css.postcss.plugins.push(plugin(opts))
|
||||
const cwd = fileURLToPath(new URL('.', import.meta.url))
|
||||
for (const pluginName of sortPlugins(postcssOptions)) {
|
||||
const pluginOptions = postcssOptions.plugins[pluginName]
|
||||
if (!pluginOptions) { continue }
|
||||
|
||||
const path = await tryResolveModule(pluginName, nuxt.options.modulesDir)
|
||||
|
||||
let pluginFn: (opts: Record<string, any>) => Plugin
|
||||
// TODO: use jiti v2
|
||||
if (path) {
|
||||
pluginFn = await import(pathToFileURL(path).href).then(interopDefault)
|
||||
} else {
|
||||
console.warn(`[nuxt] could not import postcss plugin \`${pluginName}\` with ESM. Please report this as a bug.`)
|
||||
// fall back to cjs
|
||||
pluginFn = requireModule(pluginName, { paths: [cwd] })
|
||||
}
|
||||
if (typeof pluginFn === 'function') {
|
||||
css.postcss.plugins.push(pluginFn(pluginOptions))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ import { composableKeysPlugin } from './plugins/composable-keys'
|
||||
import { logLevelMap } from './utils/logger'
|
||||
import { ssrStylesPlugin } from './plugins/ssr-styles'
|
||||
import { VitePublicDirsPlugin } from './plugins/public-dirs'
|
||||
import { distDir } from './dirs'
|
||||
|
||||
export interface ViteBuildContext {
|
||||
nuxt: Nuxt
|
||||
@ -32,6 +33,8 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
|
||||
const useAsyncEntry = nuxt.options.experimental.asyncEntry || nuxt.options.dev
|
||||
const entry = await resolvePath(resolve(nuxt.options.appDir, useAsyncEntry ? 'entry.async' : 'entry'))
|
||||
|
||||
nuxt.options.modulesDir.push(distDir)
|
||||
|
||||
let allowDirs = [
|
||||
nuxt.options.appDir,
|
||||
nuxt.options.workspaceDir,
|
||||
@ -71,7 +74,7 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
|
||||
'abort-controller': 'unenv/runtime/mock/empty',
|
||||
},
|
||||
},
|
||||
css: resolveCSSOptions(nuxt),
|
||||
css: await resolveCSSOptions(nuxt),
|
||||
define: {
|
||||
__NUXT_VERSION__: JSON.stringify(nuxt._version),
|
||||
__NUXT_ASYNC_CONTEXT__: nuxt.options.experimental.asyncContext,
|
||||
|
@ -11,11 +11,11 @@ import type { WebpackConfigContext } from '../utils/config'
|
||||
import { applyPresets } from '../utils/config'
|
||||
import { nuxt } from '../presets/nuxt'
|
||||
|
||||
export function client (ctx: WebpackConfigContext) {
|
||||
export async function client (ctx: WebpackConfigContext) {
|
||||
ctx.name = 'client'
|
||||
ctx.isClient = true
|
||||
|
||||
applyPresets(ctx, [
|
||||
await applyPresets(ctx, [
|
||||
nuxt,
|
||||
clientPlugins,
|
||||
clientOptimization,
|
||||
|
@ -9,11 +9,11 @@ import { node } from '../presets/node'
|
||||
|
||||
const assetPattern = /\.(?:css|s[ca]ss|png|jpe?g|gif|svg|woff2?|eot|ttf|otf|webp|webm|mp4|ogv)(?:\?.*)?$/i
|
||||
|
||||
export function server (ctx: WebpackConfigContext) {
|
||||
export async function server (ctx: WebpackConfigContext) {
|
||||
ctx.name = 'server'
|
||||
ctx.isServer = true
|
||||
|
||||
applyPresets(ctx, [
|
||||
await applyPresets(ctx, [
|
||||
nuxt,
|
||||
node,
|
||||
serverStandalone,
|
||||
|
@ -16,8 +16,8 @@ import WarningIgnorePlugin from '../plugins/warning-ignore'
|
||||
import type { WebpackConfigContext } from '../utils/config'
|
||||
import { applyPresets, fileName } from '../utils/config'
|
||||
|
||||
export function base (ctx: WebpackConfigContext) {
|
||||
applyPresets(ctx, [
|
||||
export async function base (ctx: WebpackConfigContext) {
|
||||
await applyPresets(ctx, [
|
||||
baseAlias,
|
||||
baseConfig,
|
||||
basePlugins,
|
||||
|
@ -8,8 +8,8 @@ import { pug } from './pug'
|
||||
import { style } from './style'
|
||||
import { vue } from './vue'
|
||||
|
||||
export function nuxt (ctx: WebpackConfigContext) {
|
||||
applyPresets(ctx, [
|
||||
export async function nuxt (ctx: WebpackConfigContext) {
|
||||
await applyPresets(ctx, [
|
||||
base,
|
||||
assets,
|
||||
esbuild,
|
||||
|
@ -4,8 +4,8 @@ import type { WebpackConfigContext } from '../utils/config'
|
||||
import { applyPresets, fileName } from '../utils/config'
|
||||
import { getPostcssConfig } from '../utils/postcss'
|
||||
|
||||
export function style (ctx: WebpackConfigContext) {
|
||||
applyPresets(ctx, [
|
||||
export async function style (ctx: WebpackConfigContext) {
|
||||
await applyPresets(ctx, [
|
||||
loaders,
|
||||
extractCSS,
|
||||
minimizer,
|
||||
@ -32,32 +32,32 @@ function extractCSS (ctx: WebpackConfigContext) {
|
||||
}))
|
||||
}
|
||||
|
||||
function loaders (ctx: WebpackConfigContext) {
|
||||
async function loaders (ctx: WebpackConfigContext) {
|
||||
// CSS
|
||||
ctx.config.module!.rules!.push(createdStyleRule('css', /\.css$/i, null, ctx))
|
||||
ctx.config.module!.rules!.push(await createdStyleRule('css', /\.css$/i, null, ctx))
|
||||
|
||||
// PostCSS
|
||||
ctx.config.module!.rules!.push(createdStyleRule('postcss', /\.p(ost)?css$/i, null, ctx))
|
||||
ctx.config.module!.rules!.push(await createdStyleRule('postcss', /\.p(ost)?css$/i, null, ctx))
|
||||
|
||||
// Less
|
||||
const lessLoader = { loader: 'less-loader', options: ctx.userConfig.loaders.less }
|
||||
ctx.config.module!.rules!.push(createdStyleRule('less', /\.less$/i, lessLoader, ctx))
|
||||
ctx.config.module!.rules!.push(await createdStyleRule('less', /\.less$/i, lessLoader, ctx))
|
||||
|
||||
// Sass (TODO: optional dependency)
|
||||
const sassLoader = { loader: 'sass-loader', options: ctx.userConfig.loaders.sass }
|
||||
ctx.config.module!.rules!.push(createdStyleRule('sass', /\.sass$/i, sassLoader, ctx))
|
||||
ctx.config.module!.rules!.push(await createdStyleRule('sass', /\.sass$/i, sassLoader, ctx))
|
||||
|
||||
const scssLoader = { loader: 'sass-loader', options: ctx.userConfig.loaders.scss }
|
||||
ctx.config.module!.rules!.push(createdStyleRule('scss', /\.scss$/i, scssLoader, ctx))
|
||||
ctx.config.module!.rules!.push(await createdStyleRule('scss', /\.scss$/i, scssLoader, ctx))
|
||||
|
||||
// Stylus
|
||||
const stylusLoader = { loader: 'stylus-loader', options: ctx.userConfig.loaders.stylus }
|
||||
ctx.config.module!.rules!.push(createdStyleRule('stylus', /\.styl(us)?$/i, stylusLoader, ctx))
|
||||
ctx.config.module!.rules!.push(await createdStyleRule('stylus', /\.styl(us)?$/i, stylusLoader, ctx))
|
||||
}
|
||||
|
||||
function createdStyleRule (lang: string, test: RegExp, processorLoader: any, ctx: WebpackConfigContext) {
|
||||
async function createdStyleRule (lang: string, test: RegExp, processorLoader: any, ctx: WebpackConfigContext) {
|
||||
const styleLoaders = [
|
||||
createPostcssLoadersRule(ctx),
|
||||
await createPostcssLoadersRule(ctx),
|
||||
processorLoader,
|
||||
].filter(Boolean)
|
||||
|
||||
@ -114,10 +114,10 @@ function createCssLoadersRule (ctx: WebpackConfigContext, cssLoaderOptions: any)
|
||||
]
|
||||
}
|
||||
|
||||
function createPostcssLoadersRule (ctx: WebpackConfigContext) {
|
||||
async function createPostcssLoadersRule (ctx: WebpackConfigContext) {
|
||||
if (!ctx.options.postcss) { return }
|
||||
|
||||
const config = getPostcssConfig(ctx.nuxt)
|
||||
const config = await getPostcssConfig(ctx.nuxt)
|
||||
|
||||
if (!config) {
|
||||
return
|
||||
|
@ -17,7 +17,7 @@ export interface WebpackConfigContext {
|
||||
transpile: RegExp[]
|
||||
}
|
||||
|
||||
type WebpackConfigPreset = (ctx: WebpackConfigContext, options?: object) => void
|
||||
type WebpackConfigPreset = (ctx: WebpackConfigContext, options?: object) => void | Promise<void>
|
||||
type WebpackConfigPresetItem = WebpackConfigPreset | [WebpackConfigPreset, any]
|
||||
|
||||
export function createWebpackConfigContext (nuxt: Nuxt): WebpackConfigContext {
|
||||
@ -37,12 +37,12 @@ export function createWebpackConfigContext (nuxt: Nuxt): WebpackConfigContext {
|
||||
}
|
||||
}
|
||||
|
||||
export function applyPresets (ctx: WebpackConfigContext, presets: WebpackConfigPresetItem | WebpackConfigPresetItem[]) {
|
||||
export async function applyPresets (ctx: WebpackConfigContext, presets: WebpackConfigPresetItem | WebpackConfigPresetItem[]) {
|
||||
for (const preset of toArray(presets)) {
|
||||
if (Array.isArray(preset)) {
|
||||
preset[0](ctx, preset[1])
|
||||
await preset[0](ctx, preset[1])
|
||||
} else {
|
||||
preset(ctx)
|
||||
await preset(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,37 +1,19 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import createResolver from 'postcss-import-resolver'
|
||||
import { requireModule } from '@nuxt/kit'
|
||||
import type { Nuxt } from '@nuxt/schema'
|
||||
import { interopDefault } from 'mlly'
|
||||
import { requireModule, tryResolveModule } from '@nuxt/kit'
|
||||
import type { Nuxt, NuxtOptions } from '@nuxt/schema'
|
||||
import { defu } from 'defu'
|
||||
import type { Plugin } from 'postcss'
|
||||
|
||||
const isPureObject = (obj: unknown): obj is Object => obj !== null && !Array.isArray(obj) && typeof obj === 'object'
|
||||
|
||||
const ensureItemIsLast = (item: string) => (arr: string[]) => {
|
||||
const index = arr.indexOf(item)
|
||||
if (index !== -1) {
|
||||
arr.splice(index, 1)
|
||||
arr.push(item)
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
const orderPresets = {
|
||||
cssnanoLast: ensureItemIsLast('cssnano'),
|
||||
autoprefixerLast: ensureItemIsLast('autoprefixer'),
|
||||
autoprefixerAndCssnanoLast (names: string[]) {
|
||||
return orderPresets.cssnanoLast(orderPresets.autoprefixerLast(names))
|
||||
},
|
||||
}
|
||||
|
||||
export const getPostcssConfig = (nuxt: Nuxt) => {
|
||||
function sortPlugins ({ plugins, order }: any) {
|
||||
function sortPlugins ({ plugins, order }: NuxtOptions['postcss']): string[] {
|
||||
const names = Object.keys(plugins)
|
||||
if (typeof order === 'string') {
|
||||
order = orderPresets[order as keyof typeof orderPresets]
|
||||
}
|
||||
return typeof order === 'function' ? order(names, orderPresets) : (order || names)
|
||||
}
|
||||
return typeof order === 'function' ? order(names) : (order || names)
|
||||
}
|
||||
|
||||
export async function getPostcssConfig (nuxt: Nuxt) {
|
||||
if (!nuxt.options.webpack.postcss || !nuxt.options.postcss) {
|
||||
return false
|
||||
}
|
||||
@ -54,21 +36,35 @@ export const getPostcssConfig = (nuxt: Nuxt) => {
|
||||
'postcss-url': {},
|
||||
},
|
||||
sourceMap: nuxt.options.webpack.cssSourceMap,
|
||||
// Array, String or Function
|
||||
order: 'autoprefixerAndCssnanoLast',
|
||||
})
|
||||
|
||||
// Keep the order of default plugins
|
||||
if (!Array.isArray(postcssOptions.plugins) && isPureObject(postcssOptions.plugins)) {
|
||||
// Map postcss plugins into instances on object mode once
|
||||
const cwd = fileURLToPath(new URL('.', import.meta.url))
|
||||
postcssOptions.plugins = sortPlugins(postcssOptions).map((pluginName: string) => {
|
||||
// TODO: remove use of requireModule in favour of ESM import
|
||||
const pluginFn = requireModule(pluginName, { paths: [cwd] })
|
||||
const plugins: Plugin[] = []
|
||||
for (const pluginName of sortPlugins(postcssOptions)) {
|
||||
const pluginOptions = postcssOptions.plugins[pluginName]
|
||||
if (!pluginOptions || typeof pluginFn !== 'function') { return null }
|
||||
return pluginFn(pluginOptions)
|
||||
}).filter(Boolean)
|
||||
if (!pluginOptions) { continue }
|
||||
|
||||
const path = await tryResolveModule(pluginName, nuxt.options.modulesDir)
|
||||
|
||||
let pluginFn: (opts: Record<string, any>) => Plugin
|
||||
// TODO: use jiti v2
|
||||
if (path) {
|
||||
pluginFn = await import(pathToFileURL(path).href).then(interopDefault)
|
||||
} else {
|
||||
console.warn(`[nuxt] could not import postcss plugin \`${pluginName}\` with ESM. Please report this as a bug.`)
|
||||
// fall back to cjs
|
||||
pluginFn = requireModule(pluginName, { paths: [cwd] })
|
||||
}
|
||||
if (typeof pluginFn === 'function') {
|
||||
plugins.push(pluginFn(pluginOptions))
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error we are mutating type here from object to array
|
||||
postcssOptions.plugins = plugins
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -28,12 +28,12 @@ import { dynamicRequire } from './nitro/plugins/dynamic-require'
|
||||
export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
|
||||
registerVirtualModules()
|
||||
|
||||
const webpackConfigs = [client, ...nuxt.options.ssr ? [server] : []].map((preset) => {
|
||||
const webpackConfigs = await Promise.all([client, ...nuxt.options.ssr ? [server] : []].map(async (preset) => {
|
||||
const ctx = createWebpackConfigContext(nuxt)
|
||||
ctx.userConfig = defu(nuxt.options.webpack[`$${preset.name as 'client' | 'server'}`], ctx.userConfig)
|
||||
applyPresets(ctx, preset)
|
||||
await applyPresets(ctx, preset)
|
||||
return getWebpackConfig(ctx)
|
||||
})
|
||||
}))
|
||||
|
||||
/** Inject rollup plugin for Nitro to handle dynamic imports from webpack chunks */
|
||||
const nitro = useNitro()
|
||||
|
@ -67,6 +67,9 @@ importers:
|
||||
'@vue/test-utils':
|
||||
specifier: 2.4.6
|
||||
version: 2.4.6
|
||||
autoprefixer:
|
||||
specifier: ^10.4.19
|
||||
version: 10.4.19(postcss@8.4.39)
|
||||
case-police:
|
||||
specifier: 0.6.1
|
||||
version: 0.6.1
|
||||
@ -76,6 +79,9 @@ importers:
|
||||
consola:
|
||||
specifier: 3.2.3
|
||||
version: 3.2.3
|
||||
cssnano:
|
||||
specifier: ^7.0.3
|
||||
version: 7.0.3(postcss@8.4.39)
|
||||
devalue:
|
||||
specifier: 5.0.0
|
||||
version: 5.0.0
|
||||
|
Loading…
Reference in New Issue
Block a user