feat: Introduce lightningcss-loader for webpack users (#61327)

### What?

I'm recreating a PR because CI of
https://github.com/vercel/next.js/pull/58712 uses `lightningcss@1.14.0`
for an unknown reason.

Add an opt-in feature to use `lightningcss` instead of webpack
css-loader.

### Why?

In the name of performance.

### How?


This PR is largely based on https://github.com/fz6m/lightningcss-loader
by @fz6m.
(Thank you for nice work)

Closes PACK-1998
Closes PACK-2124

---------

Co-authored-by: OJ Kwon <1210596+kwonoj@users.noreply.github.com>
This commit is contained in:
Donny/강동윤 2024-03-07 01:07:53 +09:00 committed by GitHub
parent 0a73e89880
commit 3ed96f92cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 2448 additions and 912 deletions

View file

@ -24,6 +24,19 @@ export function getGlobalCssLoader(
)
}
if (ctx.experimental.useLightningcss) {
loaders.push({
loader: require.resolve('../../../../loaders/lightningcss-loader/src'),
options: {
importLoaders: 1 + preProcessors.length,
url: (url: string, resourcePath: string) =>
cssFileResolve(url, resourcePath, ctx.experimental.urlImports),
import: (url: string, _: any, resourcePath: string) =>
cssFileResolve(url, resourcePath, ctx.experimental.urlImports),
targets: ctx.supportedBrowsers,
},
})
} else {
// Resolve CSS `@import`s and `url()`s
loaders.push({
loader: require.resolve('../../../../loaders/css-loader/src'),
@ -46,6 +59,7 @@ export function getGlobalCssLoader(
postcss,
},
})
}
loaders.push(
// Webpack loaders run like a stack, so we need to reverse the natural

View file

@ -24,6 +24,25 @@ export function getCssModuleLoader(
)
}
if (ctx.experimental.useLightningcss) {
loaders.push({
loader: require.resolve('../../../../loaders/lightningcss-loader/src'),
options: {
importLoaders: 1 + preProcessors.length,
url: (url: string, resourcePath: string) =>
cssFileResolve(url, resourcePath, ctx.experimental.urlImports),
import: (url: string, _: any, resourcePath: string) =>
cssFileResolve(url, resourcePath, ctx.experimental.urlImports),
modules: {
// Do not transform class names (CJS mode backwards compatibility):
exportLocalsConvention: 'asIs',
// Server-side (Node.js) rendering support:
exportOnlyLocals: ctx.isServer,
},
targets: ctx.supportedBrowsers,
},
})
} else {
// Resolve CSS `@import`s and `url()`s
loaders.push({
loader: require.resolve('../../../../loaders/css-loader/src'),
@ -61,6 +80,7 @@ export function getCssModuleLoader(
postcss,
},
})
}
loaders.push(
// Webpack loaders run like a stack, so we need to reverse the natural

View file

@ -578,4 +578,7 @@ export {
resolveRequests,
isUrlRequestable,
sort,
// For lightningcss-loader
normalizeSourceMapForRuntime,
dashesCamelCase,
}

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 fz6m
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,3 @@
# fork of lightningcss-loader
This webpack loader is largely based on https://github.com/fz6m/lightningcss-loader, which is licensed under the MIT license.

View file

@ -0,0 +1,229 @@
import type { LoaderContext } from 'next/dist/compiled/webpack/webpack'
import camelCase from '../../css-loader/src/camelcase'
import {
dashesCamelCase,
normalizeSourceMapForRuntime,
} from '../../css-loader/src/utils'
export interface CssImport {
icss?: boolean
importName: string
url: string
type?: 'url' | string
index?: number
}
export interface CssExport {
name: string
value: string
}
export interface ApiParam {
url?: string
importName?: string
layer?: string
supports?: string
media?: string
dedupe?: boolean
index?: number
}
export interface ApiReplacement {
replacementName: string
localName?: string
importName: string
needQuotes?: boolean
hash?: string
}
export function getImportCode(imports: CssImport[], options: any) {
let code = ''
for (const item of imports) {
const { importName, url, icss } = item
if (options.esModule) {
if (icss && options.modules.namedExport) {
code += `import ${
options.modules.exportOnlyLocals ? '' : `${importName}, `
}* as ${importName}_NAMED___ from ${url};\n`
} else {
code += `import ${importName} from ${url};\n`
}
} else {
code += `var ${importName} = require(${url});\n`
}
}
return code ? `// Imports\n${code}` : ''
}
export function getModuleCode(
result: { map: any; css: any },
api: ApiParam[],
replacements: ApiReplacement[],
options: any,
loaderContext: LoaderContext<any>
) {
if (options.modules.exportOnlyLocals === true) {
return ''
}
const sourceMapValue = options.sourceMap
? `,${normalizeSourceMapForRuntime(result.map, loaderContext)}`
: ''
let code = JSON.stringify(result.css)
let beforeCode = `var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(${options.sourceMap});\n`
for (const item of api) {
const { url, media, dedupe } = item
beforeCode += url
? `___CSS_LOADER_EXPORT___.push([module.id, ${JSON.stringify(
`@import url(${url});`
)}${media ? `, ${JSON.stringify(media)}` : ''}]);\n`
: `___CSS_LOADER_EXPORT___.i(${item.importName}${
media ? `, ${JSON.stringify(media)}` : dedupe ? ', ""' : ''
}${dedupe ? ', true' : ''});\n`
}
for (const item of replacements) {
const { replacementName, importName, localName } = item
if (localName) {
code = code.replace(new RegExp(replacementName, 'g'), () =>
options.modules.namedExport
? `" + ${importName}_NAMED___[${JSON.stringify(
camelCase(localName)
)}] + "`
: `" + ${importName}.locals[${JSON.stringify(localName)}] + "`
)
} else {
const { hash, needQuotes } = item
const getUrlOptions = [
...(hash ? [`hash: ${JSON.stringify(hash)}`] : []),
...(needQuotes ? 'needQuotes: true' : []),
]
const preparedOptions =
getUrlOptions.length > 0 ? `, { ${getUrlOptions.join(', ')} }` : ''
beforeCode += `var ${replacementName} = ___CSS_LOADER_GET_URL_IMPORT___(${importName}${preparedOptions});\n`
code = code.replace(
new RegExp(replacementName, 'g'),
() => `" + ${replacementName} + "`
)
}
}
return `${beforeCode}// Module\n___CSS_LOADER_EXPORT___.push([module.id, ${code}, ""${sourceMapValue}]);\n`
}
export function getExportCode(
exports: CssExport[],
replacements: ApiReplacement[],
options: any
) {
let code = '// Exports\n'
let localsCode = ''
const addExportToLocalsCode = (name: string, value: any) => {
if (options.modules.namedExport) {
localsCode += `export const ${camelCase(name)} = ${JSON.stringify(
value
)};\n`
} else {
if (localsCode) {
localsCode += `,\n`
}
localsCode += `\t${JSON.stringify(name)}: ${JSON.stringify(value)}`
}
}
for (const { name, value } of exports) {
switch (options.modules.exportLocalsConvention) {
case 'camelCase': {
addExportToLocalsCode(name, value)
const modifiedName = camelCase(name)
if (modifiedName !== name) {
addExportToLocalsCode(modifiedName, value)
}
break
}
case 'camelCaseOnly': {
addExportToLocalsCode(camelCase(name), value)
break
}
case 'dashes': {
addExportToLocalsCode(name, value)
const modifiedName = dashesCamelCase(name)
if (modifiedName !== name) {
addExportToLocalsCode(modifiedName, value)
}
break
}
case 'dashesOnly': {
addExportToLocalsCode(dashesCamelCase(name), value)
break
}
case 'asIs':
default:
addExportToLocalsCode(name, value)
break
}
}
for (const item of replacements) {
const { replacementName, localName } = item
if (localName) {
const { importName } = item
localsCode = localsCode.replace(new RegExp(replacementName, 'g'), () => {
if (options.modules.namedExport) {
return `" + ${importName}_NAMED___[${JSON.stringify(
camelCase(localName)
)}] + "`
} else if (options.modules.exportOnlyLocals) {
return `" + ${importName}[${JSON.stringify(localName)}] + "`
}
return `" + ${importName}.locals[${JSON.stringify(localName)}] + "`
})
} else {
localsCode = localsCode.replace(
new RegExp(replacementName, 'g'),
() => `" + ${replacementName} + "`
)
}
}
if (options.modules.exportOnlyLocals) {
code += options.modules.namedExport
? localsCode
: `${
options.esModule ? 'export default' : 'module.exports ='
} {\n${localsCode}\n};\n`
return code
}
if (localsCode) {
code += options.modules.namedExport
? localsCode
: `___CSS_LOADER_EXPORT___.locals = {\n${localsCode}\n};\n`
}
code += `${
options.esModule ? 'export default' : 'module.exports ='
} ___CSS_LOADER_EXPORT___;\n`
return code
}

View file

@ -0,0 +1,4 @@
import { LightningCssLoader } from './loader'
export { LightningCssMinifyPlugin } from './minify'
export default LightningCssLoader

View file

@ -0,0 +1,4 @@
export enum ECacheKey {
loader = 'loader',
minify = 'minify',
}

View file

@ -0,0 +1,490 @@
import type { LoaderContext } from 'webpack'
import { getTargets } from './utils'
import {
getImportCode,
type ApiParam,
type ApiReplacement,
type CssExport,
type CssImport,
getModuleCode,
getExportCode,
} from './codegen'
import {
getFilter,
getPreRequester,
isDataUrl,
isUrlRequestable,
requestify,
resolveRequests,
} from '../../css-loader/src/utils'
import { stringifyRequest } from '../../../stringify-request'
import { ECacheKey } from './interface'
const encoder = new TextEncoder()
const moduleRegExp = /\.module\.\w+$/i
function createUrlAndImportVisitor(
visitorOptions: any,
apis: ApiParam[],
imports: CssImport[],
replacements: ApiReplacement[],
replacedUrls: Map<number, string>,
replacedImportUrls: Map<number, string>
) {
const importUrlToNameMap = new Map<string, string>()
let hasUrlImportHelper = false
const urlToNameMap = new Map()
const urlToReplacementMap = new Map()
let urlIndex = -1
let importUrlIndex = -1
function handleUrl(u: { url: string; loc: unknown }): unknown {
let url = u.url
const needKeep = visitorOptions.urlFilter(url)
if (!needKeep) {
return u
}
if (isDataUrl(url)) {
return u
}
urlIndex++
replacedUrls.set(urlIndex, url)
url = `__NEXT_LIGHTNINGCSS_LOADER_URL_REPLACE_${urlIndex}__`
const [, query, hashOrQuery] = url.split(/(\?)?#/, 3)
const queryParts = url.split('!')
let prefix: string | undefined
if (queryParts.length > 1) {
url = queryParts.pop()!
prefix = queryParts.join('!')
}
let hash = query ? '?' : ''
hash += hashOrQuery ? `#${hashOrQuery}` : ''
if (!hasUrlImportHelper) {
imports.push({
type: 'get_url_import',
importName: '___CSS_LOADER_GET_URL_IMPORT___',
url: JSON.stringify(
require.resolve('../../css-loader/src/runtime/getUrl.js')
),
index: -1,
})
hasUrlImportHelper = true
}
const newUrl = prefix ? `${prefix}!${url}` : url
let importName = urlToNameMap.get(newUrl)
if (!importName) {
importName = `___CSS_LOADER_URL_IMPORT_${urlToNameMap.size}___`
urlToNameMap.set(newUrl, importName)
imports.push({
type: 'url',
importName,
url: JSON.stringify(newUrl),
index: urlIndex,
})
}
// This should be true for string-urls in image-set
const needQuotes = false
const replacementKey = JSON.stringify({ newUrl, hash, needQuotes })
let replacementName = urlToReplacementMap.get(replacementKey)
if (!replacementName) {
replacementName = `___CSS_LOADER_URL_REPLACEMENT_${urlToReplacementMap.size}___`
urlToReplacementMap.set(replacementKey, replacementName)
replacements.push({
replacementName,
importName,
hash,
needQuotes,
})
}
return {
loc: u.loc,
url: replacementName,
}
}
return {
Rule: {
import(node: any) {
if (visitorOptions.importFilter) {
const needKeep = visitorOptions.importFilter(
node.value.url,
node.value.media
)
if (!needKeep) {
return node
}
}
let url = node.value.url
importUrlIndex++
replacedImportUrls.set(importUrlIndex, url)
url = `__NEXT_LIGHTNINGCSS_LOADER_IMPORT_URL_REPLACE_${importUrlIndex}__`
// TODO: Use identical logic as valueParser.stringify()
const media = node.value.media.mediaQueries.length
? JSON.stringify(node.value.media.mediaQueries)
: undefined
const isRequestable = isUrlRequestable(url)
let prefix: string | undefined
if (isRequestable) {
const queryParts = url.split('!')
if (queryParts.length > 1) {
url = queryParts.pop()!
prefix = queryParts.join('!')
}
}
if (!isRequestable) {
apis.push({ url, media })
// Bug of lightningcss
return { type: 'ignored', value: '' }
}
const newUrl = prefix ? `${prefix}!${url}` : url
let importName = importUrlToNameMap.get(newUrl)
if (!importName) {
importName = `___CSS_LOADER_AT_RULE_IMPORT_${importUrlToNameMap.size}___`
importUrlToNameMap.set(newUrl, importName)
const importUrl = visitorOptions.urlHandler(newUrl)
imports.push({
type: 'rule_import',
importName,
url: importUrl,
})
}
apis.push({ importName, media })
// Bug of lightningcss
return { type: 'ignored', value: '' }
},
},
Url(node: any) {
return handleUrl(node)
},
}
}
function createIcssVisitor({
apis,
imports,
replacements,
replacedUrls,
urlHandler,
}: {
apis: ApiParam[]
imports: CssImport[]
replacements: ApiReplacement[]
replacedUrls: Map<number, string>
urlHandler: (url: any) => string
}) {
let index = -1
let replacementIndex = -1
return {
Declaration: {
composes(node: any) {
if (node.property === 'unparsed') {
return
}
const specifier = node.value.from
if (specifier?.type !== 'file') {
return
}
let url = specifier.value
if (!url) {
return
}
index++
replacedUrls.set(index, url)
url = `__NEXT_LIGHTNINGCSS_LOADER_ICSS_URL_REPLACE_${index}__`
const importName = `___CSS_LOADER_ICSS_IMPORT_${imports.length}___`
imports.push({
type: 'icss_import',
importName,
icss: true,
url: urlHandler(url),
index,
})
apis.push({ importName, dedupe: true, index })
const newNames: string[] = []
for (const localName of node.value.names) {
replacementIndex++
const replacementName = `___CSS_LOADER_ICSS_IMPORT_${index}_REPLACEMENT_${replacementIndex}___`
replacements.push({
replacementName,
importName,
localName,
})
newNames.push(replacementName)
}
return {
property: 'composes',
value: {
loc: node.value.loc,
names: newNames,
from: specifier,
},
}
},
},
}
}
const LOADER_NAME = `lightningcss-loader`
export async function LightningCssLoader(
this: LoaderContext<any>,
source: string,
prevMap?: Record<string, any>
): Promise<void> {
const done = this.async()
const options = this.getOptions()
const { implementation, targets: userTargets, ...opts } = options
options.modules ??= {}
if (implementation && typeof implementation.transformCss !== 'function') {
done(
new TypeError(
`[${LOADER_NAME}]: options.implementation.transformCss must be an 'lightningcss' transform function. Received ${typeof implementation.transformCss}`
)
)
return
}
const exports: CssExport[] = []
const imports: CssImport[] = []
const icssImports: CssImport[] = []
const apis: ApiParam[] = []
const replacements: ApiReplacement[] = []
if (options.modules?.exportOnlyLocals !== true) {
imports.unshift({
type: 'api_import',
importName: '___CSS_LOADER_API_IMPORT___',
url: stringifyRequest(
this,
require.resolve('../../css-loader/src/runtime/api')
),
})
}
const { loadBindings } = require('next/dist/build/swc')
const transform =
implementation?.transformCss ??
(await loadBindings()).css.lightning.transform
const replacedUrls = new Map<number, string>()
const icssReplacedUrls = new Map<number, string>()
const replacedImportUrls = new Map<number, string>()
const urlImportVisitor = createUrlAndImportVisitor(
{
urlHandler: (url: any) =>
stringifyRequest(
this,
getPreRequester(this)(options.importLoaders ?? 0) + url
),
urlFilter: getFilter(options.url, this.resourcePath),
importFilter: getFilter(options.import, this.resourcePath),
context: this.context,
},
apis,
imports,
replacements,
replacedUrls,
replacedImportUrls
)
const icssVisitor = createIcssVisitor({
apis,
imports: icssImports,
replacements,
replacedUrls: icssReplacedUrls,
urlHandler: (url: string) =>
stringifyRequest(
this,
getPreRequester(this)(options.importLoaders) + url
),
})
// This works by returned visitors are not conflicting.
// naive workaround for composeVisitors, as we do not directly depends on lightningcss's npm pkg
// but next-swc provides bindings
const visitor = {
...urlImportVisitor,
...icssVisitor,
}
try {
const {
code,
map,
exports: moduleExports,
} = transform({
...opts,
visitor,
cssModules:
options.modules && moduleRegExp.test(this.resourcePath)
? {
pattern: process.env.__NEXT_TEST_MODE
? '[name]__[local]'
: '[name]__[hash]__[local]',
}
: undefined,
filename: this.resourcePath,
code: encoder.encode(source),
sourceMap: this.sourceMap,
targets: getTargets({ targets: userTargets, key: ECacheKey.loader }),
inputSourceMap:
this.sourceMap && prevMap ? JSON.stringify(prevMap) : undefined,
include: 1, // Features.Nesting
})
let cssCodeAsString = code.toString()
if (moduleExports) {
for (const name in moduleExports) {
if (Object.prototype.hasOwnProperty.call(moduleExports, name)) {
const v = moduleExports[name]
let value = v.name
for (const compose of v.composes) {
value += ` ${compose.name}`
}
exports.push({
name,
value,
})
}
}
}
if (replacedUrls.size !== 0) {
const urlResolver = this.getResolve({
conditionNames: ['asset'],
mainFields: ['asset'],
mainFiles: [],
extensions: [],
})
for (const [index, url] of replacedUrls.entries()) {
const [pathname, ,] = url.split(/(\?)?#/, 3)
const request = requestify(pathname, this.rootContext)
const resolvedUrl = await resolveRequests(urlResolver, this.context, [
...new Set([request, url]),
])
for (const importItem of imports) {
importItem.url = importItem.url.replace(
`__NEXT_LIGHTNINGCSS_LOADER_URL_REPLACE_${index}__`,
resolvedUrl ?? url
)
}
}
}
if (replacedImportUrls.size !== 0) {
const importResolver = this.getResolve({
conditionNames: ['style'],
extensions: ['.css'],
mainFields: ['css', 'style', 'main', '...'],
mainFiles: ['index', '...'],
restrictions: [/\.css$/i],
})
for (const [index, url] of replacedImportUrls.entries()) {
const [pathname, ,] = url.split(/(\?)?#/, 3)
const request = requestify(pathname, this.rootContext)
const resolvedUrl = await resolveRequests(
importResolver,
this.context,
[...new Set([request, url])]
)
for (const importItem of imports) {
importItem.url = importItem.url.replace(
`__NEXT_LIGHTNINGCSS_LOADER_IMPORT_URL_REPLACE_${index}__`,
resolvedUrl ?? url
)
}
}
}
if (icssReplacedUrls.size !== 0) {
const icssResolver = this.getResolve({
conditionNames: ['style'],
extensions: [],
mainFields: ['css', 'style', 'main', '...'],
mainFiles: ['index', '...'],
})
for (const [index, url] of icssReplacedUrls.entries()) {
const [pathname, ,] = url.split(/(\?)?#/, 3)
const request = requestify(pathname, this.rootContext)
const resolvedUrl = await resolveRequests(icssResolver, this.context, [
...new Set([url, request]),
])
for (const importItem of icssImports) {
importItem.url = importItem.url.replace(
`__NEXT_LIGHTNINGCSS_LOADER_ICSS_URL_REPLACE_${index}__`,
resolvedUrl ?? url
)
}
}
}
imports.push(...icssImports)
const importCode = getImportCode(imports, options)
const moduleCode = getModuleCode(
{ css: cssCodeAsString, map },
apis,
replacements,
options,
this
)
const exportCode = getExportCode(exports, replacements, options)
const esCode = `${importCode}${moduleCode}${exportCode}`
done(null, esCode, map && JSON.parse(map.toString()))
} catch (error: unknown) {
console.error('lightningcss-loader error', error)
done(error as Error)
}
}
export const raw = true

View file

@ -0,0 +1,136 @@
// @ts-ignore
import { ModuleFilenameHelpers } from 'next/dist/compiled/webpack/webpack'
import { webpack } from 'next/dist/compiled/webpack/webpack'
// @ts-ignore
import { RawSource, SourceMapSource } from 'next/dist/compiled/webpack-sources3'
import { ECacheKey } from './interface'
import type { Compilation, Compiler } from 'webpack'
import { getTargets } from './utils'
import { Buffer } from 'buffer'
const PLUGIN_NAME = 'lightning-css-minify'
const CSS_FILE_REG = /\.css(?:\?.*)?$/i
export class LightningCssMinifyPlugin {
private readonly options: any
private transform: any | undefined
constructor(opts: any = {}) {
const { implementation, ...otherOpts } = opts
if (implementation && typeof implementation.transformCss !== 'function') {
throw new TypeError(
`[LightningCssMinifyPlugin]: implementation.transformCss must be an 'lightningcss' transform function. Received ${typeof implementation.transformCss}`
)
}
this.transform = implementation?.transformCss
this.options = otherOpts
}
apply(compiler: Compiler) {
const meta = JSON.stringify({
name: '@next/lightningcss-loader',
version: '0.0.0',
options: this.options,
})
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
compilation.hooks.chunkHash.tap(PLUGIN_NAME, (_, hash) =>
hash.update(meta)
)
compilation.hooks.processAssets.tapPromise(
{
name: PLUGIN_NAME,
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE,
additionalAssets: true,
},
async () => await this.transformAssets(compilation)
)
compilation.hooks.statsPrinter.tap(PLUGIN_NAME, (statsPrinter) => {
statsPrinter.hooks.print
.for('asset.info.minimized')
// @ts-ignore
.tap(PLUGIN_NAME, (minimized, { green, formatFlag }) => {
// @ts-ignore
return minimized ? green(formatFlag('minimized')) : undefined
})
})
})
}
private async transformAssets(compilation: Compilation): Promise<void> {
const {
options: { devtool },
} = compilation.compiler
if (!this.transform) {
const { loadBindings } = require('next/dist/build/swc')
this.transform = (await loadBindings()).css.lightning.transform
}
const sourcemap =
this.options.sourceMap === undefined
? ((devtool && (devtool as string).includes('source-map')) as boolean)
: this.options.sourceMap
const {
include,
exclude,
test: testRegExp,
targets: userTargets,
...transformOptions
} = this.options
const assets = compilation.getAssets().filter(
(asset) =>
// Filter out already minimized
!asset.info.minimized &&
// Filter out by file type
(testRegExp || CSS_FILE_REG).test(asset.name) &&
ModuleFilenameHelpers.matchObject({ include, exclude }, asset.name)
)
await Promise.all(
assets.map(async (asset) => {
const { source, map } = asset.source.sourceAndMap()
const sourceAsString = source.toString()
const code = typeof source === 'string' ? Buffer.from(source) : source
const targets = getTargets({
targets: userTargets,
key: ECacheKey.minify,
})
const result = await this.transform!({
filename: asset.name,
code,
minify: true,
sourceMap: sourcemap,
targets,
...transformOptions,
})
const codeString = result.code.toString()
compilation.updateAsset(
asset.name,
// @ts-ignore
sourcemap
? new SourceMapSource(
codeString,
asset.name,
JSON.parse(result.map!.toString()),
sourceAsString,
map as any,
true
)
: new RawSource(codeString),
{
...asset.info,
minimized: true,
}
)
})
)
}
}

View file

@ -0,0 +1,68 @@
let targetsCache: Record<string, any> = {}
/**
* Convert a version number to a single 24-bit number
*
* https://github.com/lumeland/lume/blob/4cc75599006df423a14befc06d3ed8493c645b09/plugins/lightningcss.ts#L160
*/
function version(major: number, minor = 0, patch = 0): number {
return (major << 16) | (minor << 8) | patch
}
function parseVersion(v: string) {
return v.split('.').reduce((acc, val) => {
if (!acc) {
return null
}
const parsed = parseInt(val, 10)
if (isNaN(parsed)) {
return null
}
acc.push(parsed)
return acc
}, [] as Array<number> | null)
}
function browserslistToTargets(targets: Array<string>) {
return targets.reduce((acc, value) => {
const [name, v] = value.split(' ')
const parsedVersion = parseVersion(v)
if (!parsedVersion) {
return acc
}
const versionDigit = version(
parsedVersion[0],
parsedVersion[1],
parsedVersion[2]
)
if (
name === 'and_qq' ||
name === 'and_uc' ||
name === 'baidu' ||
name === 'bb' ||
name === 'kaios' ||
name === 'op_mini'
) {
return acc
}
if (acc[name] == null || versionDigit < acc[name]) {
acc[name] = versionDigit
}
return acc
}, {} as Record<string, number>)
}
export const getTargets = (opts: { targets?: string[]; key: any }) => {
const cache = targetsCache[opts.key]
if (cache) {
return cache
}
const result = browserslistToTargets(opts.targets ?? [])
return (targetsCache[opts.key] = result)
}

View file

@ -2154,7 +2154,6 @@ packages:
/@babel/plugin-proposal-dynamic-import@7.16.7(@babel/core@7.22.5):
resolution: {integrity: sha512-I8SW9Ho3/8DRSdmDdH3gORdyUuYnk1m4cMxUAdu5oy4n3OfN8flDEH+d60iG7dUfi0KkYwSvoalHzzdRzpWHTg==}
engines: {node: '>=6.9.0'}
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-dynamic-import instead.
peerDependencies:
'@babel/core': 7.22.5
dependencies:
@ -2166,7 +2165,6 @@ packages:
/@babel/plugin-proposal-export-namespace-from@7.16.7(@babel/core@7.22.5):
resolution: {integrity: sha512-ZxdtqDXLRGBL64ocZcs7ovt71L3jhC1RGSyR996svrCi3PYqHNkb3SwPJCs8RIzD86s+WPpt2S73+EHCGO+NUA==}
engines: {node: '>=6.9.0'}
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead.
peerDependencies:
'@babel/core': 7.22.5
dependencies:
@ -2190,7 +2188,6 @@ packages:
/@babel/plugin-proposal-logical-assignment-operators@7.16.7(@babel/core@7.22.5):
resolution: {integrity: sha512-K3XzyZJGQCr00+EtYtrDjmwX7o7PLK6U9bi1nCwkQioRFVUv6dJoxbQjtWVtP+bCPy82bONBKG8NPyQ4+i6yjg==}
engines: {node: '>=6.9.0'}
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-logical-assignment-operators instead.
peerDependencies:
'@babel/core': 7.22.5
dependencies:
@ -2202,7 +2199,6 @@ packages:
/@babel/plugin-proposal-nullish-coalescing-operator@7.16.7(@babel/core@7.22.5):
resolution: {integrity: sha512-aUOrYU3EVtjf62jQrCj63pYZ7k6vns2h/DQvHPWGmsJRYzWXZ6/AsfgpiRy6XiuIDADhJzP2Q9MwSMKauBQ+UQ==}
engines: {node: '>=6.9.0'}
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.
peerDependencies:
'@babel/core': 7.22.5
dependencies:
@ -2213,7 +2209,6 @@ packages:
/@babel/plugin-proposal-numeric-separator@7.16.7(@babel/core@7.22.5):
resolution: {integrity: sha512-vQgPMknOIgiuVqbokToyXbkY/OmmjAzr/0lhSIbG/KmnzXPGwW/AdhdKpi+O4X/VkWiWjnkKOBiqJrTaC98VKw==}
engines: {node: '>=6.9.0'}
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.
peerDependencies:
'@babel/core': 7.22.5
dependencies:
@ -2252,7 +2247,6 @@ packages:
/@babel/plugin-proposal-optional-chaining@7.16.7(@babel/core@7.22.5):
resolution: {integrity: sha512-eC3xy+ZrUcBtP7x+sq62Q/HYd674pPTb/77XZMb5wbDPGWIdUbSr4Agr052+zaUPSb+gGRnjxXfKFvx5iMJ+DA==}
engines: {node: '>=6.9.0'}
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.
peerDependencies:
'@babel/core': 7.22.5
dependencies:
@ -2264,7 +2258,6 @@ packages:
/@babel/plugin-proposal-private-methods@7.16.11(@babel/core@7.22.5):
resolution: {integrity: sha512-F/2uAkPlXDr8+BHpZvo19w3hLFKge+k75XUprE6jaqKxjGkSYcK+4c+bup5PdW/7W/Rpjwql7FTVEDW+fRAQsw==}
engines: {node: '>=6.9.0'}
deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.
peerDependencies:
'@babel/core': 7.22.5
dependencies:
@ -8492,7 +8485,7 @@ packages:
engines: {node: '>= 8.0.0'}
dependencies:
find-babel-config: 1.2.0
glob: 7.1.7
glob: 7.1.6
pkg-up: 3.1.0
reselect: 4.1.8
resolve: 1.22.4
@ -12409,7 +12402,7 @@ packages:
engines: {node: '>= 10.17.0'}
hasBin: true
dependencies:
debug: 4.1.1
debug: 4.3.4
get-stream: 5.1.0
yauzl: 2.10.0
optionalDependencies:

View file

@ -1,3 +1,6 @@
import '../styles/global1.css'
import '../styles/global2.css'
export default function Root({ children }: { children: React.ReactNode }) {
return (
<html>

View file

@ -1,5 +1,14 @@
import styles from './style.module.css'
export default function Page() {
return <p className={styles.blue}>hello world</p>
console.log('CSSModules.styles', styles)
return (
<>
<p className={`search-keyword ${styles.blue}`}>hello world</p>
<div className={`${styles.blue}`}>
<div className="nested">Red due to nesting</div>
</div>
<div className="red-text">This text should be red.</div>
</>
)
}

View file

@ -1,3 +1,7 @@
.blue {
color: blue;
div {
color: red;
}
}

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="113px" height="100px" viewBox="0 0 113 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.2 (57519) - http://www.bohemiancoding.com/sketch -->
<title>Logotype - White</title>
<desc>Created with Sketch.</desc>
<defs>
<linearGradient x1="196.572434%" y1="228.815483%" x2="50%" y2="50%" id="linearGradient-1">
<stop stop-color="#000000" offset="0%"></stop>
<stop stop-color="#FFFFFF" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="White-Triangle" transform="translate(-294.000000, -150.000000)" fill="url(#linearGradient-1)">
<polygon id="Logotype---White" points="350.5 150 407 250 294 250"></polygon>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 924 B

View file

@ -1,7 +1,7 @@
import { nextTestSetup } from 'e2e-utils'
import { describeVariants as describe } from 'next-test-utils'
import { describeVariants } from 'next-test-utils'
describe.each(['turbo'])('experimental-lightningcss', () => {
describeVariants.each(['turbo'])('experimental-lightningcss', () => {
const { next } = nextTestSetup({
files: __dirname,
})
@ -11,6 +11,43 @@ describe.each(['turbo'])('experimental-lightningcss', () => {
const $ = await next.render$('/')
expect($('p').text()).toBe('hello world')
// swc_css does not include `-module` in the class name, while lightningcss does.
expect($('p').attr('class')).toBe('style-module__hlQ3RG__blue')
expect($('p').attr('class')).toBe(
'search-keyword style-module__hlQ3RG__blue'
)
})
})
// lightningcss produces different class names in turbo mode
describeVariants.each(['default'])(
'experimental-lightningcss with default mode',
() => {
describe('in dev server', () => {
const { next } = nextTestSetup({
files: __dirname,
dependencies: { lightningcss: '^1.23.0' },
packageJson: {
browserslist: ['chrome 100'],
},
})
it('should support css modules', async () => {
// Recommended for tests that check HTML. Cheerio is a HTML parser that has a jQuery like API.
const $ = await next.render$('/')
expect($('p').text()).toBe('hello world')
// We remove hash frmo the class name in test mode using env var because it is not deterministic.
expect($('p').attr('class')).toBe('search-keyword style-module__blue')
})
it('should support browserslist', async () => {
const $ = await next.browser('/')
expect(await $.elementByCss('.nested').text()).toBe(
'Red due to nesting'
)
expect(await $.elementByCss('.nested').getComputedCss('color')).toBe(
'rgb(255, 0, 0)'
)
})
})
}
)

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="114px" height="100px" viewBox="0 0 114 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.2 (57519) - http://www.bohemiancoding.com/sketch -->
<title>Logotype - Black</title>
<desc>Created with Sketch.</desc>
<defs>
<linearGradient x1="100.929941%" y1="181.283245%" x2="41.7687834%" y2="100%" id="linearGradient-1">
<stop stop-color="#FFFFFF" offset="0%"></stop>
<stop stop-color="#000000" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Black-Triangle" transform="translate(-293.000000, -150.000000)" fill="url(#linearGradient-1)">
<polygon id="Logotype---Black" points="350 150 407 250 293 250"></polygon>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 931 B

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="114px" height="100px" viewBox="0 0 114 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.2 (57519) - http://www.bohemiancoding.com/sketch -->
<title>Logotype - Black</title>
<desc>Created with Sketch.</desc>
<defs>
<linearGradient x1="100.929941%" y1="181.283245%" x2="41.7687834%" y2="100%" id="linearGradient-1">
<stop stop-color="#FFFFFF" offset="0%"></stop>
<stop stop-color="#000000" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Black-Triangle" transform="translate(-293.000000, -150.000000)" fill="url(#linearGradient-1)">
<polygon id="Logotype---Black" points="350 150 407 250 293 250"></polygon>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 931 B

View file

@ -0,0 +1,4 @@
.red-text {
color: red;
background-image: url('./dark.svg') url(dark2.svg);
}

View file

@ -0,0 +1,5 @@
@import './global2b.css';
.blue-text {
color: blue;
}

View file

@ -0,0 +1,5 @@
.blue-text {
color: orange;
font-weight: bolder;
background-image: url(../assets/light.svg);
}

View file

@ -0,0 +1 @@
next.config.js

View file

@ -0,0 +1 @@
next.config.js

View file

@ -0,0 +1 @@
next.config.js

View file

@ -0,0 +1 @@
next.config.js

View file

@ -0,0 +1 @@
next.config.js

View file

@ -0,0 +1 @@
next.config.js

View file

@ -0,0 +1 @@
next.config.js

View file

@ -0,0 +1 @@
next.config.js

View file

@ -0,0 +1 @@
next.config.js

View file

@ -0,0 +1 @@
next.config.js

View file

@ -0,0 +1 @@
next.config.js

View file

@ -0,0 +1 @@
next.config.js

View file

@ -0,0 +1 @@
next.config.js

View file

@ -0,0 +1 @@
next.config.js

View file

@ -0,0 +1 @@
next.config.js

View file

@ -0,0 +1 @@
next.config.js

View file

@ -0,0 +1 @@
next.config.js

View file

@ -0,0 +1 @@
next.config.js

View file

@ -0,0 +1 @@
next.config.js

View file

@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CSS URL via \`file-loader\` production mode useLightnincsss(false) should've emitted expected files 1`] = `".red-text{color:red;background-image:url(/_next/static/media/dark.6b01655b.svg) url(/_next/static/media/dark2.6b01655b.svg)}.blue-text{color:orange;font-weight:bolder;background-image:url(/_next/static/media/light.2da1d3d6.svg);color:blue}"`;
exports[`CSS URL via \`file-loader\` production mode useLightnincsss(true) should've emitted expected files 1`] = `".red-text{color:red;background-image:url(/_next/static/media/dark.6b01655b.svg) url(/_next/static/media/dark2.6b01655b.svg)}.blue-text{color:orange;background-image:url(/_next/static/media/light.2da1d3d6.svg);font-weight:bolder;color:#00f}"`;
exports[`Multi Global Support (reversed) production mode useLightnincsss(false) should've emitted a single CSS file 1`] = `".blue-text{color:blue}.red-text{color:red}"`;
exports[`Multi Global Support (reversed) production mode useLightnincsss(true) should've emitted a single CSS file 1`] = `".blue-text{color:#00f}.red-text{color:red}"`;
exports[`Multi Global Support production mode useLightnincsss(false) should've emitted a single CSS file 1`] = `".red-text{color:red}.blue-text{color:blue}"`;
exports[`Multi Global Support production mode useLightnincsss(true) should've emitted a single CSS file 1`] = `".red-text{color:red}.blue-text{color:#00f}"`;
exports[`Nested @import() Global Support production mode useLightnincsss(false) should've emitted a single CSS file 1`] = `".red-text{color:purple;font-weight:bolder;color:red}.blue-text{color:orange;font-weight:bolder;color:blue}"`;
exports[`Nested @import() Global Support production mode useLightnincsss(true) should've emitted a single CSS file 1`] = `".red-text{color:purple;font-weight:bolder;color:red}.blue-text{color:orange;font-weight:bolder;color:#00f}"`;

View file

@ -0,0 +1,72 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CSS Support production mode CSS Compilation and Prefixing useLightnincsss(false) should've compiled and prefixed 1`] = `"@media (min-width:480px) and (max-width:767px){::placeholder{color:green}}.flex-parsing{flex:0 0 calc(50% - var(--vertical-gutter))}.transform-parsing{transform:translate3d(0,0)}.css-grid-shorthand{grid-column:span 2}.g-docs-sidenav .filter::-webkit-input-placeholder{opacity:80%}"`;
exports[`CSS Support production mode CSS Compilation and Prefixing useLightnincsss(false) should've emitted a source map 1`] = `
{
"mappings": "AAAA,+CACE,cACE,WACF,CACF,CAEA,cACE,2CACF,CAEA,mBACE,0BACF,CAEA,oBACE,kBACF,CAEA,mDACE,WACF",
"sourcesContent": [
"@media (480px <= width < 768px) {
::placeholder {
color: green;
}
}
.flex-parsing {
flex: 0 0 calc(50% - var(--vertical-gutter));
}
.transform-parsing {
transform: translate3d(0px, 0px);
}
.css-grid-shorthand {
grid-column: span 2;
}
.g-docs-sidenav .filter::-webkit-input-placeholder {
opacity: 80%;
}
",
],
"version": 3,
}
`;
exports[`CSS Support production mode CSS Compilation and Prefixing useLightnincsss(true) should've compiled and prefixed 1`] = `"@media (min-width:480px) and (max-width:767.999px){::placeholder{color:green}}.flex-parsing{flex:0 0 calc(50% - var(--vertical-gutter))}.transform-parsing{transform:translate3d(0,0)}.css-grid-shorthand{grid-column:span 2}.g-docs-sidenav .filter::-webkit-input-placeholder{opacity:.8}"`;
exports[`CSS Support production mode CSS Compilation and Prefixing useLightnincsss(true) should've emitted a source map 1`] = `
{
"mappings": "AAAA,mDACE,cACE,WACF,CACF,CAEA,cACE,2CACF,CAEA,mBACE,0BACF,CAEA,oBACE,kBACF,CAEA,mDACE,UACF",
"sourcesContent": [
"@media (min-width: 480px) and (max-width: 767.999px) {
::placeholder {
color: green;
}
}
.flex-parsing {
flex: 0 0 calc(50% - var(--vertical-gutter));
}
.transform-parsing {
transform: translate3d(0px, 0px);
}
.css-grid-shorthand {
grid-column: span 2;
}
.g-docs-sidenav .filter::-webkit-input-placeholder {
opacity: .8;
}
",
],
"version": 3,
}
`;
exports[`CSS Support production mode Good Nested CSS Import from node_modules useLightnincsss(false) should've emitted a single CSS file 1`] = `".other{color:blue}.test{color:red}"`;
exports[`CSS Support production mode Good Nested CSS Import from node_modules useLightnincsss(true) should've emitted a single CSS file 1`] = `".other{color:#00f}.test{color:red}"`;

View file

@ -1,6 +1,6 @@
/* eslint-env jest */
import { readdir, readFile, remove } from 'fs-extra'
import { nextBuild } from 'next-test-utils'
import { nextBuild, File } from 'next-test-utils'
import { join } from 'path'
const fixturesDir = join(__dirname, '../..', 'css-fixtures')
@ -8,6 +8,21 @@ const fixturesDir = join(__dirname, '../..', 'css-fixtures')
describe('Basic Global Support', () => {
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
const appDir = join(fixturesDir, 'single-global')
const nextConfig = new File(join(appDir, 'next.config.js'))
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
beforeAll(async () => {
await remove(join(appDir, '.next'))
@ -34,10 +49,26 @@ describe('Basic Global Support', () => {
})
})
})
})
describe('Basic Global Support with special characters in path', () => {
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
const appDir = join(fixturesDir, 'single-global-special-characters', 'a+b')
const nextConfig = new File(join(appDir, 'next.config.js'))
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
beforeAll(async () => {
await remove(join(appDir, '.next'))
@ -64,10 +95,26 @@ describe('Basic Global Support with special characters in path', () => {
})
})
})
})
describe('Basic Global Support with src/ dir', () => {
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
const appDir = join(fixturesDir, 'single-global-src')
const nextConfig = new File(join(appDir, 'next.config.js'))
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
beforeAll(async () => {
await remove(join(appDir, '.next'))
@ -94,10 +141,26 @@ describe('Basic Global Support with src/ dir', () => {
})
})
})
})
describe('Multi Global Support', () => {
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
const appDir = join(fixturesDir, 'multi-global')
const nextConfig = new File(join(appDir, 'next.config.js'))
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
beforeAll(async () => {
await remove(join(appDir, '.next'))
@ -119,9 +182,8 @@ describe('Multi Global Support', () => {
expect(cssFiles.length).toBe(1)
const cssContent = await readFile(join(cssFolder, cssFiles[0]), 'utf8')
expect(
cssContent.replace(/\/\*.*?\*\//g, '').trim()
).toMatchInlineSnapshot(`".red-text{color:red}.blue-text{color:blue}"`)
expect(cssContent.replace(/\/\*.*?\*\//g, '').trim()).toMatchSnapshot()
})
})
})
})
@ -129,6 +191,21 @@ describe('Multi Global Support', () => {
describe('Nested @import() Global Support', () => {
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
const appDir = join(fixturesDir, 'nested-global')
const nextConfig = new File(join(appDir, 'next.config.js'))
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
beforeAll(async () => {
await remove(join(appDir, '.next'))
@ -150,11 +227,8 @@ describe('Nested @import() Global Support', () => {
expect(cssFiles.length).toBe(1)
const cssContent = await readFile(join(cssFolder, cssFiles[0]), 'utf8')
expect(
cssContent.replace(/\/\*.*?\*\//g, '').trim()
).toMatchInlineSnapshot(
`".red-text{color:purple;font-weight:bolder;color:red}.blue-text{color:orange;font-weight:bolder;color:blue}"`
)
expect(cssContent.replace(/\/\*.*?\*\//g, '').trim()).toMatchSnapshot()
})
})
})
})
@ -163,6 +237,21 @@ describe('Nested @import() Global Support', () => {
describe('Multi Global Support (reversed)', () => {
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
const appDir = join(fixturesDir, 'multi-global-reversed')
const nextConfig = new File(join(appDir, 'next.config.js'))
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
beforeAll(async () => {
await remove(join(appDir, '.next'))
@ -184,9 +273,8 @@ describe('Multi Global Support (reversed)', () => {
expect(cssFiles.length).toBe(1)
const cssContent = await readFile(join(cssFolder, cssFiles[0]), 'utf8')
expect(
cssContent.replace(/\/\*.*?\*\//g, '').trim()
).toMatchInlineSnapshot(`".blue-text{color:blue}.red-text{color:red}"`)
expect(cssContent.replace(/\/\*.*?\*\//g, '').trim()).toMatchSnapshot()
})
})
})
})
@ -194,6 +282,21 @@ describe('Multi Global Support (reversed)', () => {
describe('CSS URL via `file-loader`', () => {
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
const appDir = join(fixturesDir, 'url-global')
const nextConfig = new File(join(appDir, 'next.config.js'))
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
beforeAll(async () => {
await remove(join(appDir, '.next'))
@ -216,9 +319,7 @@ describe('CSS URL via `file-loader`', () => {
expect(cssFiles.length).toBe(1)
const cssContent = await readFile(join(cssFolder, cssFiles[0]), 'utf8')
expect(cssContent.replace(/\/\*.*?\*\//g, '').trim()).toMatch(
/^\.red-text\{color:red;background-image:url\(\/_next\/static\/media\/dark\.[a-f0-9]{8}\.svg\) url\(\/_next\/static\/media\/dark2\.[a-f0-9]{8}\.svg\)\}\.blue-text\{color:orange;font-weight:bolder;background-image:url\(\/_next\/static\/media\/light\.[a-f0-9]{8}\.svg\);color:blue\}$/
)
expect(cssContent.replace(/\/\*.*?\*\//g, '').trim()).toMatchSnapshot()
const mediaFiles = await readdir(mediaFolder)
expect(mediaFiles.length).toBe(3)
@ -238,6 +339,7 @@ describe('CSS URL via `file-loader`', () => {
})
})
})
})
describe('CSS URL via `file-loader` and asset prefix (1)', () => {
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {

View file

@ -3,6 +3,7 @@ import cheerio from 'cheerio'
import { readdir, readFile, remove } from 'fs-extra'
import {
findPort,
File,
killApp,
nextBuild,
nextStart,
@ -17,11 +18,30 @@ describe('CSS Support', () => {
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
describe('CSS Compilation and Prefixing', () => {
const appDir = join(fixturesDir, 'compilation-and-prefixing')
const nextConfig = new File(join(appDir, 'next.config.js'))
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
afterAll(async () => {
nextConfig.delete()
})
it('should compile successfully', async () => {
const { code, stdout } = await nextBuild(appDir, [], {
stdout: true,
@ -37,15 +57,18 @@ describe('CSS Support', () => {
const cssFiles = files.filter((f) => /\.css$/.test(f))
expect(cssFiles.length).toBe(1)
const cssContent = await readFile(join(cssFolder, cssFiles[0]), 'utf8')
const cssContent = await readFile(
join(cssFolder, cssFiles[0]),
'utf8'
)
expect(
cssContent.replace(/\/\*.*?\*\//g, '').trim()
).toMatchInlineSnapshot(
`"@media (min-width:480px) and (max-width:767px){::placeholder{color:green}}.flex-parsing{flex:0 0 calc(50% - var(--vertical-gutter))}.transform-parsing{transform:translate3d(0,0)}.css-grid-shorthand{grid-column:span 2}.g-docs-sidenav .filter::-webkit-input-placeholder{opacity:80%}"`
)
).toMatchSnapshot()
// Contains a source map
expect(cssContent).toMatch(/\/\*#\s*sourceMappingURL=(.+\.map)\s*\*\//)
expect(cssContent).toMatch(
/\/\*#\s*sourceMappingURL=(.+\.map)\s*\*\//
)
})
it(`should've emitted a source map`, async () => {
@ -59,46 +82,34 @@ describe('CSS Support', () => {
await readFile(join(cssFolder, cssMapFiles[0]), 'utf8')
).trim()
const { version, mappings, sourcesContent } = JSON.parse(cssMapContent)
expect({ version, mappings, sourcesContent }).toMatchInlineSnapshot(`
{
"mappings": "AAAA,+CACE,cACE,WACF,CACF,CAEA,cACE,2CACF,CAEA,mBACE,0BACF,CAEA,oBACE,kBACF,CAEA,mDACE,WACF",
"sourcesContent": [
"@media (480px <= width < 768px) {
::placeholder {
color: green;
}
}
.flex-parsing {
flex: 0 0 calc(50% - var(--vertical-gutter));
}
.transform-parsing {
transform: translate3d(0px, 0px);
}
.css-grid-shorthand {
grid-column: span 2;
}
.g-docs-sidenav .filter::-webkit-input-placeholder {
opacity: 80%;
}
",
],
"version": 3,
}
`)
const { version, mappings, sourcesContent } =
JSON.parse(cssMapContent)
expect({ version, mappings, sourcesContent }).toMatchSnapshot()
})
})
})
describe('React Lifecyce Order (production)', () => {
const appDir = join(fixturesDir, 'transition-react')
const nextConfig = new File(join(appDir, 'next.config.js'))
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
let appPort
let app
let code
@ -135,9 +146,25 @@ describe('CSS Support', () => {
}
})
})
})
describe('Has CSS in computed styles in Production', () => {
const appDir = join(fixturesDir, 'multi-page')
const nextConfig = new File(join(appDir, 'next.config.js'))
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
let appPort
let app
@ -181,7 +208,9 @@ describe('CSS Support', () => {
const cssSheet = $('link[rel="stylesheet"]')
expect(cssSheet.length).toBe(1)
expect(cssSheet.attr('href')).toMatch(/^\/_next\/static\/css\/.*\.css$/)
expect(cssSheet.attr('href')).toMatch(
/^\/_next\/static\/css\/.*\.css$/
)
/* ensure CSS preloaded first */
const allPreloads = [].slice.call($('link[rel="preload"]'))
@ -191,9 +220,25 @@ describe('CSS Support', () => {
expect(styleIndexes).toEqual([0])
})
})
})
describe('Good CSS Import from node_modules', () => {
const appDir = join(fixturesDir, 'npm-import')
const nextConfig = new File(join(appDir, 'next.config.js'))
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
beforeAll(async () => {
await remove(join(appDir, '.next'))
@ -214,15 +259,34 @@ describe('CSS Support', () => {
const cssFiles = files.filter((f) => /\.css$/.test(f))
expect(cssFiles.length).toBe(1)
const cssContent = await readFile(join(cssFolder, cssFiles[0]), 'utf8')
const cssContent = await readFile(
join(cssFolder, cssFiles[0]),
'utf8'
)
expect(cssContent.replace(/\/\*.*?\*\//g, '').trim()).toMatch(
/nprogress/
)
})
})
})
describe('Good Nested CSS Import from node_modules', () => {
const appDir = join(fixturesDir, 'npm-import-nested')
const nextConfig = new File(join(appDir, 'next.config.js'))
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
beforeAll(async () => {
await remove(join(appDir, '.next'))
@ -243,10 +307,14 @@ describe('CSS Support', () => {
const cssFiles = files.filter((f) => /\.css$/.test(f))
expect(cssFiles.length).toBe(1)
const cssContent = await readFile(join(cssFolder, cssFiles[0]), 'utf8')
const cssContent = await readFile(
join(cssFolder, cssFiles[0]),
'utf8'
)
expect(
cssContent.replace(/\/\*.*?\*\//g, '').trim()
).toMatchInlineSnapshot(`".other{color:blue}.test{color:red}"`)
).toMatchSnapshot()
})
})
})
})
@ -256,6 +324,21 @@ describe('CSS Support', () => {
describe('CSS Property Ordering', () => {
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
const appDir = join(fixturesDir, 'next-issue-15468')
const nextConfig = new File(join(appDir, 'next.config.js'))
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
let appPort
let app
@ -293,3 +376,4 @@ describe('CSS Property Ordering', () => {
})
})
})
})

View file

@ -20,6 +20,21 @@ const fixturesDir = join(__dirname, '../..', 'css-fixtures')
// https://github.com/vercel/next.js/issues/12343
describe('Basic CSS Modules Ordering', () => {
const appDir = join(fixturesDir, 'next-issue-12343')
const nextConfig = new File(join(appDir, 'next.config.js'))
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
let app, appPort
function tests() {
@ -78,7 +93,10 @@ describe('Basic CSS Modules Ordering', () => {
})
}
describe('Development Mode', () => {
;(process.env.TURBOPACK ? describe.skip : describe)(
'Development Mode',
() => {
// TODO(PACK-2308): Fix the ordering issue of CSS Modules in turbopack
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
@ -91,8 +109,11 @@ describe('Basic CSS Modules Ordering', () => {
})
tests()
})
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
}
)
;(process.env.TURBOPACK ? describe.skip : describe)(
'production mode',
() => {
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
@ -106,6 +127,8 @@ describe('Basic CSS Modules Ordering', () => {
})
tests()
}
)
})
})
@ -170,6 +193,21 @@ describe('Data URLs', () => {
describe('Ordering with Global CSS and Modules (dev)', () => {
const appDir = join(fixturesDir, 'global-and-module-ordering')
const nextConfig = new File(join(appDir, 'next.config.js'))
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
let appPort
let app
@ -250,10 +288,26 @@ describe('Ordering with Global CSS and Modules (dev)', () => {
}
})
})
})
describe('Ordering with Global CSS and Modules (prod)', () => {
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
const appDir = join(fixturesDir, 'global-and-module-ordering')
const nextConfig = new File(join(appDir, 'next.config.js'))
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
let appPort
let app
@ -286,6 +340,7 @@ describe('Ordering with Global CSS and Modules (prod)', () => {
})
})
})
})
// https://github.com/vercel/next.js/issues/12445
// This feature is not supported in Turbopack
@ -293,6 +348,21 @@ describe('Ordering with Global CSS and Modules (prod)', () => {
'CSS Modules Composes Ordering',
() => {
const appDir = join(fixturesDir, 'composes-ordering')
const nextConfig = new File(join(appDir, 'next.config.js'))
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
let app, appPort
function tests(isDev = false) {
@ -461,5 +531,6 @@ describe('Ordering with Global CSS and Modules (prod)', () => {
tests()
}
)
})
}
)

View file

@ -3,6 +3,7 @@ import { pathExists, readFile, readJSON, remove } from 'fs-extra'
import {
check,
findPort,
File,
killApp,
nextBuild,
nextStart,
@ -17,7 +18,21 @@ describe('CSS Support', () => {
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
describe('CSS Import from node_modules', () => {
const appDir = join(fixturesDir, 'npm-import-bad')
const nextConfig = new File(join(appDir, 'next.config.js'))
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
@ -31,6 +46,7 @@ describe('CSS Support', () => {
})
})
})
})
// https://github.com/vercel/next.js/issues/18557
describe('CSS page transition inject <style> with nonce so it works with CSP header', () => {
@ -191,6 +207,22 @@ describe('CSS Support', () => {
describe('CSS Cleanup on Render Failure', () => {
const appDir = join(fixturesDir, 'transition-cleanup')
const nextConfig = new File(join(appDir, 'next.config.js'))
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
let app, appPort
function tests() {
@ -254,9 +286,25 @@ describe('CSS Support', () => {
}
)
})
})
describe('Page reload on CSS missing', () => {
const appDir = join(fixturesDir, 'transition-reload')
const nextConfig = new File(join(appDir, 'next.config.js'))
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
let app, appPort
function tests() {
@ -306,7 +354,9 @@ describe('CSS Support', () => {
e.endsWith('.css')
)
if (files.length < 1) throw new Error()
await Promise.all(files.map((f) => remove(join(appDir, '.next', f))))
await Promise.all(
files.map((f) => remove(join(appDir, '.next', f)))
)
})
afterAll(async () => {
await killApp(app)
@ -316,9 +366,26 @@ describe('CSS Support', () => {
}
)
})
})
describe('Page hydrates with CSS and not waiting on dependencies', () => {
const appDir = join(fixturesDir, 'hydrate-without-deps')
const nextConfig = new File(join(appDir, 'next.config.js'))
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
let app, appPort
function tests() {
@ -417,3 +484,4 @@ describe('CSS Support', () => {
)
})
})
})