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,28 +24,42 @@ export function getGlobalCssLoader(
)
}
// Resolve CSS `@import`s and `url()`s
loaders.push({
loader: require.resolve('../../../../loaders/css-loader/src'),
options: {
postcss,
importLoaders: 1 + preProcessors.length,
// Next.js controls CSS Modules eligibility:
modules: false,
url: (url: string, resourcePath: string) =>
cssFileResolve(url, resourcePath, ctx.experimental.urlImports),
import: (url: string, _: any, resourcePath: string) =>
cssFileResolve(url, resourcePath, ctx.experimental.urlImports),
},
})
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'),
options: {
postcss,
importLoaders: 1 + preProcessors.length,
// Next.js controls CSS Modules eligibility:
modules: false,
url: (url: string, resourcePath: string) =>
cssFileResolve(url, resourcePath, ctx.experimental.urlImports),
import: (url: string, _: any, resourcePath: string) =>
cssFileResolve(url, resourcePath, ctx.experimental.urlImports),
},
})
// Compile CSS
loaders.push({
loader: require.resolve('../../../../loaders/postcss-loader/src'),
options: {
postcss,
},
})
// Compile CSS
loaders.push({
loader: require.resolve('../../../../loaders/postcss-loader/src'),
options: {
postcss,
},
})
}
loaders.push(
// Webpack loaders run like a stack, so we need to reverse the natural

View file

@ -24,43 +24,63 @@ export function getCssModuleLoader(
)
}
// Resolve CSS `@import`s and `url()`s
loaders.push({
loader: require.resolve('../../../../loaders/css-loader/src'),
options: {
postcss,
importLoaders: 1 + preProcessors.length,
// Use CJS mode for backwards compatibility:
esModule: false,
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,
// Disallow global style exports so we can code-split CSS and
// not worry about loading order.
mode: 'pure',
// Generate a friendly production-ready name so it's
// reasonably understandable. The same name is used for
// development.
// TODO: Consider making production reduce this to a single
// character?
getLocalIdent: getCssModuleLocalIdent,
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'),
options: {
postcss,
importLoaders: 1 + preProcessors.length,
// Use CJS mode for backwards compatibility:
esModule: false,
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,
// Disallow global style exports so we can code-split CSS and
// not worry about loading order.
mode: 'pure',
// Generate a friendly production-ready name so it's
// reasonably understandable. The same name is used for
// development.
// TODO: Consider making production reduce this to a single
// character?
getLocalIdent: getCssModuleLocalIdent,
},
},
})
// Compile CSS
loaders.push({
loader: require.resolve('../../../../loaders/postcss-loader/src'),
options: {
postcss,
},
})
// Compile CSS
loaders.push({
loader: require.resolve('../../../../loaders/postcss-loader/src'),
options: {
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,29 +8,45 @@ 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'))
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
it('should compile successfully', async () => {
const { code, stdout } = await nextBuild(appDir, [], {
stdout: true,
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
})
it(`should've emitted a single CSS file`, async () => {
const cssFolder = join(appDir, '.next/static/css')
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
const files = await readdir(cssFolder)
const cssFiles = files.filter((f) => /\.css$/.test(f))
it('should compile successfully', async () => {
const { code, stdout } = await nextBuild(appDir, [], {
stdout: true,
})
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
})
expect(cssFiles.length).toBe(1)
expect(await readFile(join(cssFolder, cssFiles[0]), 'utf8')).toContain(
'color:red'
)
it(`should've emitted a single CSS file`, async () => {
const cssFolder = join(appDir, '.next/static/css')
const files = await readdir(cssFolder)
const cssFiles = files.filter((f) => /\.css$/.test(f))
expect(cssFiles.length).toBe(1)
expect(await readFile(join(cssFolder, cssFiles[0]), 'utf8')).toContain(
'color:red'
)
})
})
})
})
@ -38,29 +54,45 @@ 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'))
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
it('should compile successfully', async () => {
const { code, stdout } = await nextBuild(appDir, [], {
stdout: true,
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
})
it(`should've emitted a single CSS file`, async () => {
const cssFolder = join(appDir, '.next/static/css')
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
const files = await readdir(cssFolder)
const cssFiles = files.filter((f) => /\.css$/.test(f))
it('should compile successfully', async () => {
const { code, stdout } = await nextBuild(appDir, [], {
stdout: true,
})
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
})
expect(cssFiles.length).toBe(1)
expect(await readFile(join(cssFolder, cssFiles[0]), 'utf8')).toContain(
'color:red'
)
it(`should've emitted a single CSS file`, async () => {
const cssFolder = join(appDir, '.next/static/css')
const files = await readdir(cssFolder)
const cssFiles = files.filter((f) => /\.css$/.test(f))
expect(cssFiles.length).toBe(1)
expect(await readFile(join(cssFolder, cssFiles[0]), 'utf8')).toContain(
'color:red'
)
})
})
})
})
@ -68,29 +100,45 @@ 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'))
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
it('should compile successfully', async () => {
const { code, stdout } = await nextBuild(appDir, [], {
stdout: true,
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
})
it(`should've emitted a single CSS file`, async () => {
const cssFolder = join(appDir, '.next/static/css')
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
const files = await readdir(cssFolder)
const cssFiles = files.filter((f) => /\.css$/.test(f))
it('should compile successfully', async () => {
const { code, stdout } = await nextBuild(appDir, [], {
stdout: true,
})
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
})
expect(cssFiles.length).toBe(1)
expect(await readFile(join(cssFolder, cssFiles[0]), 'utf8')).toContain(
'color:red'
)
it(`should've emitted a single CSS file`, async () => {
const cssFolder = join(appDir, '.next/static/css')
const files = await readdir(cssFolder)
const cssFiles = files.filter((f) => /\.css$/.test(f))
expect(cssFiles.length).toBe(1)
expect(await readFile(join(cssFolder, cssFiles[0]), 'utf8')).toContain(
'color:red'
)
})
})
})
})
@ -98,30 +146,44 @@ 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'))
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
it('should compile successfully', async () => {
const { code, stdout } = await nextBuild(appDir, [], {
stdout: true,
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
})
it(`should've emitted a single CSS file`, async () => {
const cssFolder = join(appDir, '.next/static/css')
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
const files = await readdir(cssFolder)
const cssFiles = files.filter((f) => /\.css$/.test(f))
it('should compile successfully', async () => {
const { code, stdout } = await nextBuild(appDir, [], {
stdout: true,
})
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
})
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}"`)
it(`should've emitted a single CSS file`, async () => {
const cssFolder = join(appDir, '.next/static/css')
const files = await readdir(cssFolder)
const cssFiles = files.filter((f) => /\.css$/.test(f))
expect(cssFiles.length).toBe(1)
const cssContent = await readFile(join(cssFolder, cssFiles[0]), 'utf8')
expect(cssContent.replace(/\/\*.*?\*\//g, '').trim()).toMatchSnapshot()
})
})
})
})
@ -129,32 +191,44 @@ 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'))
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
it('should compile successfully', async () => {
const { code, stdout } = await nextBuild(appDir, [], {
stdout: true,
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
})
it(`should've emitted a single CSS file`, async () => {
const cssFolder = join(appDir, '.next/static/css')
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
const files = await readdir(cssFolder)
const cssFiles = files.filter((f) => /\.css$/.test(f))
it('should compile successfully', async () => {
const { code, stdout } = await nextBuild(appDir, [], {
stdout: true,
})
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
})
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}"`
)
it(`should've emitted a single CSS file`, async () => {
const cssFolder = join(appDir, '.next/static/css')
const files = await readdir(cssFolder)
const cssFiles = files.filter((f) => /\.css$/.test(f))
expect(cssFiles.length).toBe(1)
const cssContent = await readFile(join(cssFolder, cssFiles[0]), 'utf8')
expect(cssContent.replace(/\/\*.*?\*\//g, '').trim()).toMatchSnapshot()
})
})
})
})
@ -163,30 +237,44 @@ 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'))
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
it('should compile successfully', async () => {
const { code, stdout } = await nextBuild(appDir, [], {
stdout: true,
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
})
it(`should've emitted a single CSS file`, async () => {
const cssFolder = join(appDir, '.next/static/css')
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
const files = await readdir(cssFolder)
const cssFiles = files.filter((f) => /\.css$/.test(f))
it('should compile successfully', async () => {
const { code, stdout } = await nextBuild(appDir, [], {
stdout: true,
})
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
})
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}"`)
it(`should've emitted a single CSS file`, async () => {
const cssFolder = join(appDir, '.next/static/css')
const files = await readdir(cssFolder)
const cssFiles = files.filter((f) => /\.css$/.test(f))
expect(cssFiles.length).toBe(1)
const cssContent = await readFile(join(cssFolder, cssFiles[0]), 'utf8')
expect(cssContent.replace(/\/\*.*?\*\//g, '').trim()).toMatchSnapshot()
})
})
})
})
@ -194,47 +282,61 @@ 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'))
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
it('should compile successfully', async () => {
const { code, stdout } = await nextBuild(appDir, [], {
stdout: true,
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
})
it(`should've emitted expected files`, async () => {
const cssFolder = join(appDir, '.next/static/css')
const mediaFolder = join(appDir, '.next/static/media')
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
const files = await readdir(cssFolder)
const cssFiles = files.filter((f) => /\.css$/.test(f))
it('should compile successfully', async () => {
const { code, stdout } = await nextBuild(appDir, [], {
stdout: true,
})
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
})
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\}$/
)
it(`should've emitted expected files`, async () => {
const cssFolder = join(appDir, '.next/static/css')
const mediaFolder = join(appDir, '.next/static/media')
const mediaFiles = await readdir(mediaFolder)
expect(mediaFiles.length).toBe(3)
expect(
mediaFiles
.map((fileName) =>
/^(.+?)\..{8}\.(.+?)$/.exec(fileName).slice(1).join('.')
)
.sort()
).toMatchInlineSnapshot(`
const files = await readdir(cssFolder)
const cssFiles = files.filter((f) => /\.css$/.test(f))
expect(cssFiles.length).toBe(1)
const cssContent = await readFile(join(cssFolder, cssFiles[0]), 'utf8')
expect(cssContent.replace(/\/\*.*?\*\//g, '').trim()).toMatchSnapshot()
const mediaFiles = await readdir(mediaFolder)
expect(mediaFiles.length).toBe(3)
expect(
mediaFiles
.map((fileName) =>
/^(.+?)\..{8}\.(.+?)$/.exec(fileName).slice(1).join('.')
)
.sort()
).toMatchInlineSnapshot(`
[
"dark.svg",
"dark2.svg",
"light.svg",
]
`)
})
})
})
})
@ -277,12 +379,12 @@ describe('CSS URL via `file-loader` and asset prefix (1)', () => {
)
.sort()
).toMatchInlineSnapshot(`
[
"dark.svg",
"dark2.svg",
"light.svg",
]
`)
[
"dark.svg",
"dark2.svg",
"light.svg",
]
`)
})
})
})
@ -325,12 +427,12 @@ describe('CSS URL via `file-loader` and asset prefix (2)', () => {
)
.sort()
).toMatchInlineSnapshot(`
[
"dark.svg",
"dark2.svg",
"light.svg",
]
`)
[
"dark.svg",
"dark2.svg",
"light.svg",
]
`)
})
})
})

View file

@ -3,6 +3,7 @@ import cheerio from 'cheerio'
import { readdir, readFile, remove } from 'fs-extra'
import {
findPort,
File,
killApp,
nextBuild,
nextStart,
@ -17,236 +18,303 @@ 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'))
})
it('should compile successfully', async () => {
const { code, stdout } = await nextBuild(appDir, [], {
stdout: true,
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
})
it(`should've compiled and prefixed`, async () => {
const cssFolder = join(appDir, '.next/static/css')
afterAll(async () => {
nextConfig.delete()
})
const files = await readdir(cssFolder)
const cssFiles = files.filter((f) => /\.css$/.test(f))
it('should compile successfully', async () => {
const { code, stdout } = await nextBuild(appDir, [], {
stdout: true,
})
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
})
expect(cssFiles.length).toBe(1)
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%}"`
)
it(`should've compiled and prefixed`, async () => {
const cssFolder = join(appDir, '.next/static/css')
// Contains a source map
expect(cssContent).toMatch(/\/\*#\s*sourceMappingURL=(.+\.map)\s*\*\//)
})
const files = await readdir(cssFolder)
const cssFiles = files.filter((f) => /\.css$/.test(f))
it(`should've emitted a source map`, async () => {
const cssFolder = join(appDir, '.next/static/css')
expect(cssFiles.length).toBe(1)
const cssContent = await readFile(
join(cssFolder, cssFiles[0]),
'utf8'
)
expect(
cssContent.replace(/\/\*.*?\*\//g, '').trim()
).toMatchSnapshot()
const files = await readdir(cssFolder)
const cssMapFiles = files.filter((f) => /\.css\.map$/.test(f))
// Contains a source map
expect(cssContent).toMatch(
/\/\*#\s*sourceMappingURL=(.+\.map)\s*\*\//
)
})
expect(cssMapFiles.length).toBe(1)
const cssMapContent = (
await readFile(join(cssFolder, cssMapFiles[0]), 'utf8')
).trim()
it(`should've emitted a source map`, async () => {
const cssFolder = join(appDir, '.next/static/css')
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;
}
}
const files = await readdir(cssFolder)
const cssMapFiles = files.filter((f) => /\.css\.map$/.test(f))
.flex-parsing {
flex: 0 0 calc(50% - var(--vertical-gutter));
}
expect(cssMapFiles.length).toBe(1)
const cssMapContent = (
await readFile(join(cssFolder, cssMapFiles[0]), 'utf8')
).trim()
.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'))
})
let appPort
let app
let code
let stdout
beforeAll(async () => {
;({ code, stdout } = await nextBuild(appDir, [], {
stdout: true,
}))
appPort = await findPort()
app = await nextStart(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
})
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
it('should have compiled successfully', () => {
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
})
let appPort
let app
let code
let stdout
beforeAll(async () => {
;({ code, stdout } = await nextBuild(appDir, [], {
stdout: true,
}))
appPort = await findPort()
app = await nextStart(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
})
it('should have the correct color on mount after navigation', async () => {
let browser
try {
browser = await webdriver(appPort, '/')
it('should have compiled successfully', () => {
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
})
// Navigate to other:
await browser.waitForElementByCss('#link-other').click()
const text = await browser.waitForElementByCss('#red-title').text()
expect(text).toMatchInlineSnapshot(`"rgb(255, 0, 0)"`)
} finally {
if (browser) {
await browser.close()
it('should have the correct color on mount after navigation', async () => {
let browser
try {
browser = await webdriver(appPort, '/')
// Navigate to other:
await browser.waitForElementByCss('#link-other').click()
const text = await browser.waitForElementByCss('#red-title').text()
expect(text).toMatchInlineSnapshot(`"rgb(255, 0, 0)"`)
} finally {
if (browser) {
await browser.close()
}
}
}
})
})
})
describe('Has CSS in computed styles in Production', () => {
const appDir = join(fixturesDir, 'multi-page')
const nextConfig = new File(join(appDir, 'next.config.js'))
let appPort
let app
let stdout
let code
beforeAll(async () => {
await remove(join(appDir, '.next'))
;({ code, stdout } = await nextBuild(appDir, [], {
stdout: true,
}))
appPort = await findPort()
app = await nextStart(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
})
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
it('should have compiled successfully', () => {
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
})
let appPort
let app
let stdout
let code
beforeAll(async () => {
await remove(join(appDir, '.next'))
;({ code, stdout } = await nextBuild(appDir, [], {
stdout: true,
}))
appPort = await findPort()
app = await nextStart(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
})
it('should have CSS for page', async () => {
const browser = await webdriver(appPort, '/page2')
it('should have compiled successfully', () => {
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
})
const currentColor = await browser.eval(
`window.getComputedStyle(document.querySelector('.blue-text')).color`
)
expect(currentColor).toMatchInlineSnapshot(`"rgb(0, 0, 255)"`)
})
it('should have CSS for page', async () => {
const browser = await webdriver(appPort, '/page2')
it(`should've preloaded the CSS file and injected it in <head>`, async () => {
const content = await renderViaHTTP(appPort, '/page2')
const $ = cheerio.load(content)
const currentColor = await browser.eval(
`window.getComputedStyle(document.querySelector('.blue-text')).color`
)
expect(currentColor).toMatchInlineSnapshot(`"rgb(0, 0, 255)"`)
})
const cssPreload = $('link[rel="preload"][as="style"]')
expect(cssPreload.length).toBe(1)
expect(cssPreload.attr('href')).toMatch(
/^\/_next\/static\/css\/.*\.css$/
)
it(`should've preloaded the CSS file and injected it in <head>`, async () => {
const content = await renderViaHTTP(appPort, '/page2')
const $ = cheerio.load(content)
const cssSheet = $('link[rel="stylesheet"]')
expect(cssSheet.length).toBe(1)
expect(cssSheet.attr('href')).toMatch(/^\/_next\/static\/css\/.*\.css$/)
const cssPreload = $('link[rel="preload"][as="style"]')
expect(cssPreload.length).toBe(1)
expect(cssPreload.attr('href')).toMatch(
/^\/_next\/static\/css\/.*\.css$/
)
/* ensure CSS preloaded first */
const allPreloads = [].slice.call($('link[rel="preload"]'))
const styleIndexes = allPreloads.flatMap((p, i) =>
p.attribs.as === 'style' ? i : []
)
expect(styleIndexes).toEqual([0])
const cssSheet = $('link[rel="stylesheet"]')
expect(cssSheet.length).toBe(1)
expect(cssSheet.attr('href')).toMatch(
/^\/_next\/static\/css\/.*\.css$/
)
/* ensure CSS preloaded first */
const allPreloads = [].slice.call($('link[rel="preload"]'))
const styleIndexes = allPreloads.flatMap((p, i) =>
p.attribs.as === 'style' ? i : []
)
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'))
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
it('should compile successfully', async () => {
const { code, stdout } = await nextBuild(appDir, [], {
stdout: true,
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
})
it(`should've emitted a single CSS file`, async () => {
const cssFolder = join(appDir, '.next/static/css')
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
const files = await readdir(cssFolder)
const cssFiles = files.filter((f) => /\.css$/.test(f))
it('should compile successfully', async () => {
const { code, stdout } = await nextBuild(appDir, [], {
stdout: true,
})
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
})
expect(cssFiles.length).toBe(1)
const cssContent = await readFile(join(cssFolder, cssFiles[0]), 'utf8')
expect(cssContent.replace(/\/\*.*?\*\//g, '').trim()).toMatch(
/nprogress/
)
it(`should've emitted a single CSS file`, async () => {
const cssFolder = join(appDir, '.next/static/css')
const files = await readdir(cssFolder)
const cssFiles = files.filter((f) => /\.css$/.test(f))
expect(cssFiles.length).toBe(1)
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'))
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
it('should compile successfully', async () => {
const { code, stdout } = await nextBuild(appDir, [], {
stdout: true,
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
})
it(`should've emitted a single CSS file`, async () => {
const cssFolder = join(appDir, '.next/static/css')
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
const files = await readdir(cssFolder)
const cssFiles = files.filter((f) => /\.css$/.test(f))
it('should compile successfully', async () => {
const { code, stdout } = await nextBuild(appDir, [], {
stdout: true,
})
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
})
expect(cssFiles.length).toBe(1)
const cssContent = await readFile(join(cssFolder, cssFiles[0]), 'utf8')
expect(
cssContent.replace(/\/\*.*?\*\//g, '').trim()
).toMatchInlineSnapshot(`".other{color:blue}.test{color:red}"`)
it(`should've emitted a single CSS file`, async () => {
const cssFolder = join(appDir, '.next/static/css')
const files = await readdir(cssFolder)
const cssFiles = files.filter((f) => /\.css$/.test(f))
expect(cssFiles.length).toBe(1)
const cssContent = await readFile(
join(cssFolder, cssFiles[0]),
'utf8'
)
expect(
cssContent.replace(/\/\*.*?\*\//g, '').trim()
).toMatchSnapshot()
})
})
})
})
@ -256,40 +324,56 @@ 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'))
let appPort
let app
let stdout
let code
beforeAll(async () => {
await remove(join(appDir, '.next'))
;({ code, stdout } = await nextBuild(appDir, [], {
stdout: true,
}))
appPort = await findPort()
app = await nextStart(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
})
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
})
it('should have compiled successfully', () => {
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
})
let appPort
let app
let stdout
let code
beforeAll(async () => {
await remove(join(appDir, '.next'))
;({ code, stdout } = await nextBuild(appDir, [], {
stdout: true,
}))
appPort = await findPort()
app = await nextStart(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
})
it('should have the border width (property ordering)', async () => {
const browser = await webdriver(appPort, '/')
it('should have compiled successfully', () => {
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
})
const width1 = await browser.eval(
`window.getComputedStyle(document.querySelector('.test1')).borderWidth`
)
expect(width1).toMatchInlineSnapshot(`"0px"`)
it('should have the border width (property ordering)', async () => {
const browser = await webdriver(appPort, '/')
const width2 = await browser.eval(
`window.getComputedStyle(document.querySelector('.test2')).borderWidth`
)
expect(width2).toMatchInlineSnapshot(`"5px"`)
const width1 = await browser.eval(
`window.getComputedStyle(document.querySelector('.test1')).borderWidth`
)
expect(width1).toMatchInlineSnapshot(`"0px"`)
const width2 = await browser.eval(
`window.getComputedStyle(document.querySelector('.test2')).borderWidth`
)
expect(width2).toMatchInlineSnapshot(`"5px"`)
})
})
})
})

View file

@ -20,92 +20,115 @@ 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')
let app, appPort
const nextConfig = new File(join(appDir, 'next.config.js'))
function tests() {
async function checkGreenButton(browser) {
await browser.waitForElementByCss('#link-other')
const titleColor = await browser.eval(
`window.getComputedStyle(document.querySelector('#link-other')).backgroundColor`
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
expect(titleColor).toBe('rgb(0, 255, 0)')
}
async function checkPinkButton(browser) {
await browser.waitForElementByCss('#link-index')
const titleColor = await browser.eval(
`window.getComputedStyle(document.querySelector('#link-index')).backgroundColor`
)
expect(titleColor).toBe('rgb(255, 105, 180)')
}
it('should have correct color on index page (on load)', async () => {
const browser = await webdriver(appPort, '/')
try {
await checkGreenButton(browser)
} finally {
await browser.close()
}
})
let app, appPort
it('should have correct color on index page (on hover)', async () => {
const browser = await webdriver(appPort, '/')
try {
await checkGreenButton(browser)
await browser.waitForElementByCss('#link-other').moveTo()
await waitFor(2000)
await checkGreenButton(browser)
} finally {
await browser.close()
function tests() {
async function checkGreenButton(browser) {
await browser.waitForElementByCss('#link-other')
const titleColor = await browser.eval(
`window.getComputedStyle(document.querySelector('#link-other')).backgroundColor`
)
expect(titleColor).toBe('rgb(0, 255, 0)')
}
})
it('should have correct color on index page (on nav)', async () => {
const browser = await webdriver(appPort, '/')
try {
await checkGreenButton(browser)
await browser.waitForElementByCss('#link-other').click()
// Wait for navigation:
async function checkPinkButton(browser) {
await browser.waitForElementByCss('#link-index')
await checkPinkButton(browser)
// Navigate back to index:
await browser.waitForElementByCss('#link-index').click()
await checkGreenButton(browser)
} finally {
await browser.close()
const titleColor = await browser.eval(
`window.getComputedStyle(document.querySelector('#link-index')).backgroundColor`
)
expect(titleColor).toBe('rgb(255, 105, 180)')
}
})
}
describe('Development Mode', () => {
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
beforeAll(async () => {
appPort = await findPort()
app = await launchApp(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
})
it('should have correct color on index page (on load)', async () => {
const browser = await webdriver(appPort, '/')
try {
await checkGreenButton(browser)
} finally {
await browser.close()
}
})
tests()
})
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
beforeAll(async () => {
await nextBuild(appDir, [], {})
appPort = await findPort()
app = await nextStart(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
})
it('should have correct color on index page (on hover)', async () => {
const browser = await webdriver(appPort, '/')
try {
await checkGreenButton(browser)
await browser.waitForElementByCss('#link-other').moveTo()
await waitFor(2000)
await checkGreenButton(browser)
} finally {
await browser.close()
}
})
tests()
it('should have correct color on index page (on nav)', async () => {
const browser = await webdriver(appPort, '/')
try {
await checkGreenButton(browser)
await browser.waitForElementByCss('#link-other').click()
// Wait for navigation:
await browser.waitForElementByCss('#link-index')
await checkPinkButton(browser)
// Navigate back to index:
await browser.waitForElementByCss('#link-index').click()
await checkGreenButton(browser)
} finally {
await browser.close()
}
})
}
;(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'))
})
beforeAll(async () => {
appPort = await findPort()
app = await launchApp(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
})
tests()
}
)
;(process.env.TURBOPACK ? describe.skip : describe)(
'production mode',
() => {
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
beforeAll(async () => {
await nextBuild(appDir, [], {})
appPort = await findPort()
app = await nextStart(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
})
tests()
}
)
})
})
@ -170,110 +193,48 @@ 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'))
let appPort
let app
beforeAll(async () => {
await remove(join(appDir, '.next'))
appPort = await findPort()
app = await launchApp(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
})
it('should not execute scripts in any order', async () => {
const content = await renderViaHTTP(appPort, '/')
const $ = cheerio.load(content)
let asyncCount = 0
let totalCount = 0
for (const script of $('script').toArray()) {
++totalCount
if ('async' in script.attribs) {
++asyncCount
}
}
expect(asyncCount).toBe(0)
expect(totalCount).not.toBe(0)
})
it('should have the correct color (css ordering)', async () => {
const browser = await webdriver(appPort, '/')
const currentColor = await browser.eval(
`window.getComputedStyle(document.querySelector('#blueText')).color`
)
expect(currentColor).toMatchInlineSnapshot(`"rgb(0, 0, 255)"`)
})
it('should have the correct color (css ordering) during hot reloads', async () => {
let browser
try {
browser = await webdriver(appPort, '/')
const blueColor = await browser.eval(
`window.getComputedStyle(document.querySelector('#blueText')).color`
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
expect(blueColor).toMatchInlineSnapshot(`"rgb(0, 0, 255)"`)
const yellowColor = await browser.eval(
`window.getComputedStyle(document.querySelector('#yellowText')).color`
)
expect(yellowColor).toMatchInlineSnapshot(`"rgb(255, 255, 0)"`)
const cssFile = new File(join(appDir, 'pages/index.module.css'))
try {
cssFile.replace('color: yellow;', 'color: rgb(1, 1, 1);')
await check(
() =>
browser.eval(
`window.getComputedStyle(document.querySelector('#yellowText')).color`
),
'rgb(1, 1, 1)'
)
await check(
() =>
browser.eval(
`window.getComputedStyle(document.querySelector('#blueText')).color`
),
'rgb(0, 0, 255)'
)
} finally {
cssFile.restore()
}
} finally {
if (browser) {
await browser.close()
}
}
})
})
describe('Ordering with Global CSS and Modules (prod)', () => {
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
const appDir = join(fixturesDir, 'global-and-module-ordering')
})
let appPort
let app
let stdout
let code
beforeAll(async () => {
await remove(join(appDir, '.next'))
;({ code, stdout } = await nextBuild(appDir, [], {
stdout: true,
}))
appPort = await findPort()
app = await nextStart(appDir, appPort)
app = await launchApp(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
})
it('should have compiled successfully', () => {
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
it('should not execute scripts in any order', async () => {
const content = await renderViaHTTP(appPort, '/')
const $ = cheerio.load(content)
let asyncCount = 0
let totalCount = 0
for (const script of $('script').toArray()) {
++totalCount
if ('async' in script.attribs) {
++asyncCount
}
}
expect(asyncCount).toBe(0)
expect(totalCount).not.toBe(0)
})
it('should have the correct color (css ordering)', async () => {
@ -284,6 +245,100 @@ describe('Ordering with Global CSS and Modules (prod)', () => {
)
expect(currentColor).toMatchInlineSnapshot(`"rgb(0, 0, 255)"`)
})
it('should have the correct color (css ordering) during hot reloads', async () => {
let browser
try {
browser = await webdriver(appPort, '/')
const blueColor = await browser.eval(
`window.getComputedStyle(document.querySelector('#blueText')).color`
)
expect(blueColor).toMatchInlineSnapshot(`"rgb(0, 0, 255)"`)
const yellowColor = await browser.eval(
`window.getComputedStyle(document.querySelector('#yellowText')).color`
)
expect(yellowColor).toMatchInlineSnapshot(`"rgb(255, 255, 0)"`)
const cssFile = new File(join(appDir, 'pages/index.module.css'))
try {
cssFile.replace('color: yellow;', 'color: rgb(1, 1, 1);')
await check(
() =>
browser.eval(
`window.getComputedStyle(document.querySelector('#yellowText')).color`
),
'rgb(1, 1, 1)'
)
await check(
() =>
browser.eval(
`window.getComputedStyle(document.querySelector('#blueText')).color`
),
'rgb(0, 0, 255)'
)
} finally {
cssFile.restore()
}
} finally {
if (browser) {
await browser.close()
}
}
})
})
})
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
let stdout
let code
beforeAll(async () => {
await remove(join(appDir, '.next'))
;({ code, stdout } = await nextBuild(appDir, [], {
stdout: true,
}))
appPort = await findPort()
app = await nextStart(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
})
it('should have compiled successfully', () => {
expect(code).toBe(0)
expect(stdout).toMatch(/Compiled successfully/)
})
it('should have the correct color (css ordering)', async () => {
const browser = await webdriver(appPort, '/')
const currentColor = await browser.eval(
`window.getComputedStyle(document.querySelector('#blueText')).color`
)
expect(currentColor).toMatchInlineSnapshot(`"rgb(0, 0, 255)"`)
})
})
})
})
@ -293,47 +348,49 @@ describe('Ordering with Global CSS and Modules (prod)', () => {
'CSS Modules Composes Ordering',
() => {
const appDir = join(fixturesDir, 'composes-ordering')
let app, appPort
const nextConfig = new File(join(appDir, 'next.config.js'))
function tests(isDev = false) {
async function checkBlackTitle(browser) {
await browser.waitForElementByCss('#black-title')
const titleColor = await browser.eval(
`window.getComputedStyle(document.querySelector('#black-title')).color`
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
expect(titleColor).toBe('rgb(17, 17, 17)')
}
async function checkRedTitle(browser) {
await browser.waitForElementByCss('#red-title')
const titleColor = await browser.eval(
`window.getComputedStyle(document.querySelector('#red-title')).color`
)
expect(titleColor).toBe('rgb(255, 0, 0)')
}
it('should have correct color on index page (on load)', async () => {
const browser = await webdriver(appPort, '/')
try {
await checkBlackTitle(browser)
} finally {
await browser.close()
}
})
let app, appPort
it('should have correct color on index page (on hover)', async () => {
const browser = await webdriver(appPort, '/')
try {
await checkBlackTitle(browser)
await browser.waitForElementByCss('#link-other').moveTo()
await waitFor(2000)
await checkBlackTitle(browser)
} finally {
await browser.close()
function tests(isDev = false) {
async function checkBlackTitle(browser) {
await browser.waitForElementByCss('#black-title')
const titleColor = await browser.eval(
`window.getComputedStyle(document.querySelector('#black-title')).color`
)
expect(titleColor).toBe('rgb(17, 17, 17)')
}
async function checkRedTitle(browser) {
await browser.waitForElementByCss('#red-title')
const titleColor = await browser.eval(
`window.getComputedStyle(document.querySelector('#red-title')).color`
)
expect(titleColor).toBe('rgb(255, 0, 0)')
}
})
if (!isDev) {
it('should not change color on hover', async () => {
it('should have correct color on index page (on load)', async () => {
const browser = await webdriver(appPort, '/')
try {
await checkBlackTitle(browser)
} finally {
await browser.close()
}
})
it('should have correct color on index page (on hover)', async () => {
const browser = await webdriver(appPort, '/')
try {
await checkBlackTitle(browser)
@ -345,121 +402,135 @@ describe('Ordering with Global CSS and Modules (prod)', () => {
}
})
it('should have correct CSS injection order', async () => {
if (!isDev) {
it('should not change color on hover', async () => {
const browser = await webdriver(appPort, '/')
try {
await checkBlackTitle(browser)
await browser.waitForElementByCss('#link-other').moveTo()
await waitFor(2000)
await checkBlackTitle(browser)
} finally {
await browser.close()
}
})
it('should have correct CSS injection order', async () => {
const browser = await webdriver(appPort, '/')
try {
await checkBlackTitle(browser)
const prevSiblingHref = await browser.eval(
`document.querySelector('link[rel=stylesheet][data-n-p]').previousSibling.getAttribute('href')`
)
const currentPageHref = await browser.eval(
`document.querySelector('link[rel=stylesheet][data-n-p]').getAttribute('href')`
)
expect(prevSiblingHref).toBeDefined()
expect(prevSiblingHref).toBe(currentPageHref)
// Navigate to other:
await browser.waitForElementByCss('#link-other').click()
await checkRedTitle(browser)
const newPrevSibling = await browser.eval(
`document.querySelector('style[data-n-href]').previousSibling.getAttribute('data-n-css')`
)
const newPageHref = await browser.eval(
`document.querySelector('style[data-n-href]').getAttribute('data-n-href')`
)
expect(newPrevSibling).toBe('')
expect(newPageHref).toBeDefined()
expect(newPageHref).not.toBe(currentPageHref)
// Navigate to home:
await browser.waitForElementByCss('#link-index').click()
await checkBlackTitle(browser)
const newPrevSibling2 = await browser.eval(
`document.querySelector('style[data-n-href]').previousSibling.getAttribute('data-n-css')`
)
const newPageHref2 = await browser.eval(
`document.querySelector('style[data-n-href]').getAttribute('data-n-href')`
)
expect(newPrevSibling2).toBe('')
expect(newPageHref2).toBeDefined()
expect(newPageHref2).toBe(currentPageHref)
} finally {
await browser.close()
}
})
}
it('should have correct color on index page (on nav from index)', async () => {
const browser = await webdriver(appPort, '/')
try {
await checkBlackTitle(browser)
const prevSiblingHref = await browser.eval(
`document.querySelector('link[rel=stylesheet][data-n-p]').previousSibling.getAttribute('href')`
)
const currentPageHref = await browser.eval(
`document.querySelector('link[rel=stylesheet][data-n-p]').getAttribute('href')`
)
expect(prevSiblingHref).toBeDefined()
expect(prevSiblingHref).toBe(currentPageHref)
// Navigate to other:
await browser.waitForElementByCss('#link-other').click()
// Wait for navigation:
await browser.waitForElementByCss('#link-index')
await checkRedTitle(browser)
const newPrevSibling = await browser.eval(
`document.querySelector('style[data-n-href]').previousSibling.getAttribute('data-n-css')`
)
const newPageHref = await browser.eval(
`document.querySelector('style[data-n-href]').getAttribute('data-n-href')`
)
expect(newPrevSibling).toBe('')
expect(newPageHref).toBeDefined()
expect(newPageHref).not.toBe(currentPageHref)
// Navigate to home:
// Navigate back to index:
await browser.waitForElementByCss('#link-index').click()
await checkBlackTitle(browser)
} finally {
await browser.close()
}
})
const newPrevSibling2 = await browser.eval(
`document.querySelector('style[data-n-href]').previousSibling.getAttribute('data-n-css')`
)
const newPageHref2 = await browser.eval(
`document.querySelector('style[data-n-href]').getAttribute('data-n-href')`
)
expect(newPrevSibling2).toBe('')
expect(newPageHref2).toBeDefined()
expect(newPageHref2).toBe(currentPageHref)
it('should have correct color on index page (on nav from other)', async () => {
const browser = await webdriver(appPort, '/other')
try {
await checkRedTitle(browser)
await browser.waitForElementByCss('#link-index').click()
// Wait for navigation:
await browser.waitForElementByCss('#link-other')
await checkBlackTitle(browser)
// Navigate back to other:
await browser.waitForElementByCss('#link-other').click()
await checkRedTitle(browser)
} finally {
await browser.close()
}
})
}
it('should have correct color on index page (on nav from index)', async () => {
const browser = await webdriver(appPort, '/')
try {
await checkBlackTitle(browser)
await browser.waitForElementByCss('#link-other').click()
// Wait for navigation:
await browser.waitForElementByCss('#link-index')
await checkRedTitle(browser)
// Navigate back to index:
await browser.waitForElementByCss('#link-index').click()
await checkBlackTitle(browser)
} finally {
await browser.close()
}
})
it('should have correct color on index page (on nav from other)', async () => {
const browser = await webdriver(appPort, '/other')
try {
await checkRedTitle(browser)
await browser.waitForElementByCss('#link-index').click()
// Wait for navigation:
await browser.waitForElementByCss('#link-other')
await checkBlackTitle(browser)
// Navigate back to other:
await browser.waitForElementByCss('#link-other').click()
await checkRedTitle(browser)
} finally {
await browser.close()
}
})
}
describe('Development Mode', () => {
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
beforeAll(async () => {
appPort = await findPort()
app = await launchApp(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
})
tests(true)
})
;(process.env.TURBOPACK ? describe.skip : describe)(
'production mode',
() => {
describe('Development Mode', () => {
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
beforeAll(async () => {
await nextBuild(appDir, [], {})
appPort = await findPort()
app = await nextStart(appDir, appPort)
app = await launchApp(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
})
tests()
}
)
tests(true)
})
;(process.env.TURBOPACK ? describe.skip : describe)(
'production mode',
() => {
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
beforeAll(async () => {
await nextBuild(appDir, [], {})
appPort = await findPort()
app = await nextStart(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
})
tests()
}
)
})
}
)

View file

@ -3,6 +3,7 @@ import { pathExists, readFile, readJSON, remove } from 'fs-extra'
import {
check,
findPort,
File,
killApp,
nextBuild,
nextStart,
@ -17,17 +18,32 @@ 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'))
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}
}
}`
)
})
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
it('should fail the build', async () => {
const { code, stderr } = await nextBuild(appDir, [], { stderr: true })
it('should fail the build', async () => {
const { code, stderr } = await nextBuild(appDir, [], { stderr: true })
expect(code).toBe(0)
expect(stderr).not.toMatch(/Can't resolve '[^']*?nprogress[^']*?'/)
expect(stderr).not.toMatch(/Build error occurred/)
expect(code).toBe(0)
expect(stderr).not.toMatch(/Can't resolve '[^']*?nprogress[^']*?'/)
expect(stderr).not.toMatch(/Build error occurred/)
})
})
})
})
@ -191,229 +207,281 @@ describe('CSS Support', () => {
describe('CSS Cleanup on Render Failure', () => {
const appDir = join(fixturesDir, 'transition-cleanup')
let app, appPort
const nextConfig = new File(join(appDir, 'next.config.js'))
function tests() {
async function checkBlackTitle(browser) {
await browser.waitForElementByCss('#black-title')
const titleColor = await browser.eval(
`window.getComputedStyle(document.querySelector('#black-title')).color`
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
expect(titleColor).toBe('rgb(17, 17, 17)')
}
it('not have intermediary page styles on error rendering', async () => {
const browser = await webdriver(appPort, '/')
try {
await checkBlackTitle(browser)
const currentPageStyles = await browser.eval(
`document.querySelector('link[rel=stylesheet][data-n-p]')`
)
expect(currentPageStyles).toBeDefined()
// Navigate to other:
await browser.waitForElementByCss('#link-other').click()
await check(
() => browser.eval(`document.body.innerText`),
'Application error: a client-side exception has occurred (see the browser console for more information).',
true
)
const newPageStyles = await browser.eval(
`document.querySelector('link[rel=stylesheet][data-n-p]')`
)
expect(newPageStyles).toBeFalsy()
const allPageStyles = await browser.eval(
`document.querySelector('link[rel=stylesheet]')`
)
expect(allPageStyles).toBeFalsy()
} finally {
await browser.close()
}
})
}
;(process.env.TURBOPACK ? describe.skip : describe)(
'production mode',
() => {
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
beforeAll(async () => {
await nextBuild(appDir, [], {})
appPort = await findPort()
app = await nextStart(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
})
let app, appPort
tests()
function tests() {
async function checkBlackTitle(browser) {
await browser.waitForElementByCss('#black-title')
const titleColor = await browser.eval(
`window.getComputedStyle(document.querySelector('#black-title')).color`
)
expect(titleColor).toBe('rgb(17, 17, 17)')
}
it('not have intermediary page styles on error rendering', async () => {
const browser = await webdriver(appPort, '/')
try {
await checkBlackTitle(browser)
const currentPageStyles = await browser.eval(
`document.querySelector('link[rel=stylesheet][data-n-p]')`
)
expect(currentPageStyles).toBeDefined()
// Navigate to other:
await browser.waitForElementByCss('#link-other').click()
await check(
() => browser.eval(`document.body.innerText`),
'Application error: a client-side exception has occurred (see the browser console for more information).',
true
)
const newPageStyles = await browser.eval(
`document.querySelector('link[rel=stylesheet][data-n-p]')`
)
expect(newPageStyles).toBeFalsy()
const allPageStyles = await browser.eval(
`document.querySelector('link[rel=stylesheet]')`
)
expect(allPageStyles).toBeFalsy()
} finally {
await browser.close()
}
})
}
)
;(process.env.TURBOPACK ? describe.skip : describe)(
'production mode',
() => {
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
beforeAll(async () => {
await nextBuild(appDir, [], {})
appPort = await findPort()
app = await nextStart(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
})
tests()
}
)
})
})
describe('Page reload on CSS missing', () => {
const appDir = join(fixturesDir, 'transition-reload')
let app, appPort
const nextConfig = new File(join(appDir, 'next.config.js'))
function tests() {
async function checkBlackTitle(browser) {
await browser.waitForElementByCss('#black-title')
const titleColor = await browser.eval(
`window.getComputedStyle(document.querySelector('#black-title')).color`
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
expect(titleColor).toBe('rgb(17, 17, 17)')
}
it('should fall back to server-side transition on missing CSS', async () => {
const browser = await webdriver(appPort, '/')
try {
await checkBlackTitle(browser)
await browser.eval(`window.__priorNavigatePageState = 'OOF';`)
// Navigate to other:
await browser.waitForElementByCss('#link-other').click()
// Wait for navigation:
await browser.waitForElementByCss('#link-index')
const state = await browser.eval(`window.__priorNavigatePageState`)
expect(state).toBeFalsy()
} finally {
await browser.close()
}
})
}
let app, appPort
;(process.env.TURBOPACK ? describe.skip : describe)(
'production mode',
() => {
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
beforeAll(async () => {
await nextBuild(appDir, [], {})
appPort = await findPort()
app = await nextStart(appDir, appPort)
// Remove other page CSS files:
const manifest = await readJSON(
join(appDir, '.next', 'build-manifest.json')
function tests() {
async function checkBlackTitle(browser) {
await browser.waitForElementByCss('#black-title')
const titleColor = await browser.eval(
`window.getComputedStyle(document.querySelector('#black-title')).color`
)
const files = manifest['pages']['/other'].filter((e) =>
e.endsWith('.css')
)
if (files.length < 1) throw new Error()
await Promise.all(files.map((f) => remove(join(appDir, '.next', f))))
})
afterAll(async () => {
await killApp(app)
})
expect(titleColor).toBe('rgb(17, 17, 17)')
}
tests()
it('should fall back to server-side transition on missing CSS', async () => {
const browser = await webdriver(appPort, '/')
try {
await checkBlackTitle(browser)
await browser.eval(`window.__priorNavigatePageState = 'OOF';`)
// Navigate to other:
await browser.waitForElementByCss('#link-other').click()
// Wait for navigation:
await browser.waitForElementByCss('#link-index')
const state = await browser.eval(`window.__priorNavigatePageState`)
expect(state).toBeFalsy()
} finally {
await browser.close()
}
})
}
)
;(process.env.TURBOPACK ? describe.skip : describe)(
'production mode',
() => {
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
beforeAll(async () => {
await nextBuild(appDir, [], {})
appPort = await findPort()
app = await nextStart(appDir, appPort)
// Remove other page CSS files:
const manifest = await readJSON(
join(appDir, '.next', 'build-manifest.json')
)
const files = manifest['pages']['/other'].filter((e) =>
e.endsWith('.css')
)
if (files.length < 1) throw new Error()
await Promise.all(
files.map((f) => remove(join(appDir, '.next', f)))
)
})
afterAll(async () => {
await killApp(app)
})
tests()
}
)
})
})
describe('Page hydrates with CSS and not waiting on dependencies', () => {
const appDir = join(fixturesDir, 'hydrate-without-deps')
let app, appPort
const nextConfig = new File(join(appDir, 'next.config.js'))
function tests() {
async function checkBlackTitle(browser) {
await browser.waitForElementByCss('#black-title')
const titleColor = await browser.eval(
`window.getComputedStyle(document.querySelector('#black-title')).color`
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
beforeAll(async () => {
nextConfig.write(
`
const config = require('../next.config.js');
module.exports = {
...config,
experimental: {
useLightningcss: ${useLightningcss}
}
}`
)
expect(titleColor).toBe('rgb(17, 17, 17)')
}
async function checkRedTitle(browser) {
await browser.waitForElementByCss('#red-title')
const titleColor = await browser.eval(
`window.getComputedStyle(document.querySelector('#red-title')).color`
)
expect(titleColor).toBe('rgb(255, 0, 0)')
}
it('should hydrate black without dependencies manifest', async () => {
const browser = await webdriver(appPort, '/')
try {
await checkBlackTitle(browser)
await check(
() => browser.eval(`document.querySelector('p').innerText`),
'mounted'
)
} finally {
await browser.close()
}
})
it('should hydrate red without dependencies manifest', async () => {
const browser = await webdriver(appPort, '/client')
try {
await checkRedTitle(browser)
await check(
() => browser.eval(`document.querySelector('p').innerText`),
'mounted'
let app, appPort
function tests() {
async function checkBlackTitle(browser) {
await browser.waitForElementByCss('#black-title')
const titleColor = await browser.eval(
`window.getComputedStyle(document.querySelector('#black-title')).color`
)
} finally {
await browser.close()
expect(titleColor).toBe('rgb(17, 17, 17)')
}
})
it('should route from black to red without dependencies', async () => {
const browser = await webdriver(appPort, '/')
try {
await checkBlackTitle(browser)
await check(
() => browser.eval(`document.querySelector('p').innerText`),
'mounted'
async function checkRedTitle(browser) {
await browser.waitForElementByCss('#red-title')
const titleColor = await browser.eval(
`window.getComputedStyle(document.querySelector('#red-title')).color`
)
await browser.eval(`document.querySelector('#link-client').click()`)
await checkRedTitle(browser)
await check(
() => browser.eval(`document.querySelector('p').innerText`),
'mounted'
)
} finally {
await browser.close()
expect(titleColor).toBe('rgb(255, 0, 0)')
}
})
}
;(process.env.TURBOPACK ? describe.skip : describe)(
'production mode',
() => {
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
beforeAll(async () => {
await nextBuild(appDir, [], {})
appPort = await findPort()
app = await nextStart(appDir, appPort)
const buildId = (
await readFile(join(appDir, '.next', 'BUILD_ID'), 'utf8')
).trim()
const fileName = join(
appDir,
'.next/static/',
buildId,
'_buildManifest.js'
)
if (!(await pathExists(fileName))) {
throw new Error('Missing build manifest')
it('should hydrate black without dependencies manifest', async () => {
const browser = await webdriver(appPort, '/')
try {
await checkBlackTitle(browser)
await check(
() => browser.eval(`document.querySelector('p').innerText`),
'mounted'
)
} finally {
await browser.close()
}
await remove(fileName)
})
afterAll(async () => {
await killApp(app)
})
tests()
it('should hydrate red without dependencies manifest', async () => {
const browser = await webdriver(appPort, '/client')
try {
await checkRedTitle(browser)
await check(
() => browser.eval(`document.querySelector('p').innerText`),
'mounted'
)
} finally {
await browser.close()
}
})
it('should route from black to red without dependencies', async () => {
const browser = await webdriver(appPort, '/')
try {
await checkBlackTitle(browser)
await check(
() => browser.eval(`document.querySelector('p').innerText`),
'mounted'
)
await browser.eval(`document.querySelector('#link-client').click()`)
await checkRedTitle(browser)
await check(
() => browser.eval(`document.querySelector('p').innerText`),
'mounted'
)
} finally {
await browser.close()
}
})
}
)
;(process.env.TURBOPACK ? describe.skip : describe)(
'production mode',
() => {
beforeAll(async () => {
await remove(join(appDir, '.next'))
})
beforeAll(async () => {
await nextBuild(appDir, [], {})
appPort = await findPort()
app = await nextStart(appDir, appPort)
const buildId = (
await readFile(join(appDir, '.next', 'BUILD_ID'), 'utf8')
).trim()
const fileName = join(
appDir,
'.next/static/',
buildId,
'_buildManifest.js'
)
if (!(await pathExists(fileName))) {
throw new Error('Missing build manifest')
}
await remove(fileName)
})
afterAll(async () => {
await killApp(app)
})
tests()
}
)
})
})
})