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:
parent
0a73e89880
commit
3ed96f92cb
47 changed files with 2448 additions and 912 deletions
|
@ -24,6 +24,19 @@ export function getGlobalCssLoader(
|
|||
)
|
||||
}
|
||||
|
||||
if (ctx.experimental.useLightningcss) {
|
||||
loaders.push({
|
||||
loader: require.resolve('../../../../loaders/lightningcss-loader/src'),
|
||||
options: {
|
||||
importLoaders: 1 + preProcessors.length,
|
||||
url: (url: string, resourcePath: string) =>
|
||||
cssFileResolve(url, resourcePath, ctx.experimental.urlImports),
|
||||
import: (url: string, _: any, resourcePath: string) =>
|
||||
cssFileResolve(url, resourcePath, ctx.experimental.urlImports),
|
||||
targets: ctx.supportedBrowsers,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// Resolve CSS `@import`s and `url()`s
|
||||
loaders.push({
|
||||
loader: require.resolve('../../../../loaders/css-loader/src'),
|
||||
|
@ -46,6 +59,7 @@ export function getGlobalCssLoader(
|
|||
postcss,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
loaders.push(
|
||||
// Webpack loaders run like a stack, so we need to reverse the natural
|
||||
|
|
|
@ -24,6 +24,25 @@ export function getCssModuleLoader(
|
|||
)
|
||||
}
|
||||
|
||||
if (ctx.experimental.useLightningcss) {
|
||||
loaders.push({
|
||||
loader: require.resolve('../../../../loaders/lightningcss-loader/src'),
|
||||
options: {
|
||||
importLoaders: 1 + preProcessors.length,
|
||||
url: (url: string, resourcePath: string) =>
|
||||
cssFileResolve(url, resourcePath, ctx.experimental.urlImports),
|
||||
import: (url: string, _: any, resourcePath: string) =>
|
||||
cssFileResolve(url, resourcePath, ctx.experimental.urlImports),
|
||||
modules: {
|
||||
// Do not transform class names (CJS mode backwards compatibility):
|
||||
exportLocalsConvention: 'asIs',
|
||||
// Server-side (Node.js) rendering support:
|
||||
exportOnlyLocals: ctx.isServer,
|
||||
},
|
||||
targets: ctx.supportedBrowsers,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// Resolve CSS `@import`s and `url()`s
|
||||
loaders.push({
|
||||
loader: require.resolve('../../../../loaders/css-loader/src'),
|
||||
|
@ -61,6 +80,7 @@ export function getCssModuleLoader(
|
|||
postcss,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
loaders.push(
|
||||
// Webpack loaders run like a stack, so we need to reverse the natural
|
||||
|
|
|
@ -578,4 +578,7 @@ export {
|
|||
resolveRequests,
|
||||
isUrlRequestable,
|
||||
sort,
|
||||
// For lightningcss-loader
|
||||
normalizeSourceMapForRuntime,
|
||||
dashesCamelCase,
|
||||
}
|
||||
|
|
|
@ -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.
|
|
@ -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.
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import { LightningCssLoader } from './loader'
|
||||
|
||||
export { LightningCssMinifyPlugin } from './minify'
|
||||
export default LightningCssLoader
|
|
@ -0,0 +1,4 @@
|
|||
export enum ECacheKey {
|
||||
loader = 'loader',
|
||||
minify = 'minify',
|
||||
}
|
|
@ -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
|
|
@ -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,
|
||||
}
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import '../styles/global1.css'
|
||||
import '../styles/global2.css'
|
||||
|
||||
export default function Root({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html>
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
.blue {
|
||||
color: blue;
|
||||
|
||||
div {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 |
|
@ -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)'
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -0,0 +1,4 @@
|
|||
.red-text {
|
||||
color: red;
|
||||
background-image: url('./dark.svg') url(dark2.svg);
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
@import './global2b.css';
|
||||
|
||||
.blue-text {
|
||||
color: blue;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
.blue-text {
|
||||
color: orange;
|
||||
font-weight: bolder;
|
||||
background-image: url(../assets/light.svg);
|
||||
}
|
1
test/integration/css-fixtures/composes-ordering/.gitignore
vendored
Normal file
1
test/integration/css-fixtures/composes-ordering/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
next.config.js
|
1
test/integration/css-fixtures/global-and-module-ordering/.gitignore
vendored
Normal file
1
test/integration/css-fixtures/global-and-module-ordering/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
next.config.js
|
1
test/integration/css-fixtures/hydrate-without-deps/.gitignore
vendored
Normal file
1
test/integration/css-fixtures/hydrate-without-deps/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
next.config.js
|
1
test/integration/css-fixtures/multi-global-reversed/.gitignore
vendored
Normal file
1
test/integration/css-fixtures/multi-global-reversed/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
next.config.js
|
1
test/integration/css-fixtures/multi-global/.gitignore
vendored
Normal file
1
test/integration/css-fixtures/multi-global/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
next.config.js
|
1
test/integration/css-fixtures/multi-page/.gitignore
vendored
Normal file
1
test/integration/css-fixtures/multi-page/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
next.config.js
|
1
test/integration/css-fixtures/nested-global/.gitignore
vendored
Normal file
1
test/integration/css-fixtures/nested-global/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
next.config.js
|
1
test/integration/css-fixtures/next-issue-12343/.gitignore
vendored
Normal file
1
test/integration/css-fixtures/next-issue-12343/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
next.config.js
|
1
test/integration/css-fixtures/next-issue-15468/.gitignore
vendored
Normal file
1
test/integration/css-fixtures/next-issue-15468/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
next.config.js
|
1
test/integration/css-fixtures/npm-import-bad/.gitignore
vendored
Normal file
1
test/integration/css-fixtures/npm-import-bad/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
next.config.js
|
1
test/integration/css-fixtures/npm-import-nested/.gitignore
vendored
Normal file
1
test/integration/css-fixtures/npm-import-nested/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
next.config.js
|
1
test/integration/css-fixtures/npm-import/.gitignore
vendored
Normal file
1
test/integration/css-fixtures/npm-import/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
next.config.js
|
1
test/integration/css-fixtures/single-global-special-characters/a+b/.gitignore
vendored
Normal file
1
test/integration/css-fixtures/single-global-special-characters/a+b/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
next.config.js
|
1
test/integration/css-fixtures/single-global-src/.gitignore
vendored
Normal file
1
test/integration/css-fixtures/single-global-src/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
next.config.js
|
1
test/integration/css-fixtures/single-global/.gitignore
vendored
Normal file
1
test/integration/css-fixtures/single-global/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
next.config.js
|
1
test/integration/css-fixtures/transition-cleanup/.gitignore
vendored
Normal file
1
test/integration/css-fixtures/transition-cleanup/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
next.config.js
|
1
test/integration/css-fixtures/transition-react/.gitignore
vendored
Normal file
1
test/integration/css-fixtures/transition-react/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
next.config.js
|
1
test/integration/css-fixtures/transition-reload/.gitignore
vendored
Normal file
1
test/integration/css-fixtures/transition-reload/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
next.config.js
|
1
test/integration/css-fixtures/url-global/.gitignore
vendored
Normal file
1
test/integration/css-fixtures/url-global/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
next.config.js
|
|
@ -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}"`;
|
|
@ -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}"`;
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-env jest */
|
||||
import { readdir, readFile, remove } from 'fs-extra'
|
||||
import { nextBuild } from 'next-test-utils'
|
||||
import { nextBuild, File } from 'next-test-utils'
|
||||
import { join } from 'path'
|
||||
|
||||
const fixturesDir = join(__dirname, '../..', 'css-fixtures')
|
||||
|
@ -8,6 +8,21 @@ const fixturesDir = join(__dirname, '../..', 'css-fixtures')
|
|||
describe('Basic Global Support', () => {
|
||||
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
|
||||
const appDir = join(fixturesDir, 'single-global')
|
||||
const nextConfig = new File(join(appDir, 'next.config.js'))
|
||||
|
||||
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
|
||||
beforeAll(async () => {
|
||||
nextConfig.write(
|
||||
`
|
||||
const config = require('../next.config.js');
|
||||
module.exports = {
|
||||
...config,
|
||||
experimental: {
|
||||
useLightningcss: ${useLightningcss}
|
||||
}
|
||||
}`
|
||||
)
|
||||
})
|
||||
|
||||
beforeAll(async () => {
|
||||
await remove(join(appDir, '.next'))
|
||||
|
@ -34,10 +49,26 @@ describe('Basic Global Support', () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Basic Global Support with special characters in path', () => {
|
||||
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
|
||||
const appDir = join(fixturesDir, 'single-global-special-characters', 'a+b')
|
||||
const nextConfig = new File(join(appDir, 'next.config.js'))
|
||||
|
||||
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
|
||||
beforeAll(async () => {
|
||||
nextConfig.write(
|
||||
`
|
||||
const config = require('../../next.config.js');
|
||||
module.exports = {
|
||||
...config,
|
||||
experimental: {
|
||||
useLightningcss: ${useLightningcss}
|
||||
}
|
||||
}`
|
||||
)
|
||||
})
|
||||
|
||||
beforeAll(async () => {
|
||||
await remove(join(appDir, '.next'))
|
||||
|
@ -64,10 +95,26 @@ describe('Basic Global Support with special characters in path', () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Basic Global Support with src/ dir', () => {
|
||||
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
|
||||
const appDir = join(fixturesDir, 'single-global-src')
|
||||
const nextConfig = new File(join(appDir, 'next.config.js'))
|
||||
|
||||
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
|
||||
beforeAll(async () => {
|
||||
nextConfig.write(
|
||||
`
|
||||
const config = require('../next.config.js');
|
||||
module.exports = {
|
||||
...config,
|
||||
experimental: {
|
||||
useLightningcss: ${useLightningcss}
|
||||
}
|
||||
}`
|
||||
)
|
||||
})
|
||||
|
||||
beforeAll(async () => {
|
||||
await remove(join(appDir, '.next'))
|
||||
|
@ -94,10 +141,26 @@ describe('Basic Global Support with src/ dir', () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Multi Global Support', () => {
|
||||
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
|
||||
const appDir = join(fixturesDir, 'multi-global')
|
||||
const nextConfig = new File(join(appDir, 'next.config.js'))
|
||||
|
||||
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
|
||||
beforeAll(async () => {
|
||||
nextConfig.write(
|
||||
`
|
||||
const config = require('../next.config.js');
|
||||
module.exports = {
|
||||
...config,
|
||||
experimental: {
|
||||
useLightningcss: ${useLightningcss}
|
||||
}
|
||||
}`
|
||||
)
|
||||
})
|
||||
|
||||
beforeAll(async () => {
|
||||
await remove(join(appDir, '.next'))
|
||||
|
@ -119,9 +182,8 @@ describe('Multi Global Support', () => {
|
|||
|
||||
expect(cssFiles.length).toBe(1)
|
||||
const cssContent = await readFile(join(cssFolder, cssFiles[0]), 'utf8')
|
||||
expect(
|
||||
cssContent.replace(/\/\*.*?\*\//g, '').trim()
|
||||
).toMatchInlineSnapshot(`".red-text{color:red}.blue-text{color:blue}"`)
|
||||
expect(cssContent.replace(/\/\*.*?\*\//g, '').trim()).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -129,6 +191,21 @@ describe('Multi Global Support', () => {
|
|||
describe('Nested @import() Global Support', () => {
|
||||
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
|
||||
const appDir = join(fixturesDir, 'nested-global')
|
||||
const nextConfig = new File(join(appDir, 'next.config.js'))
|
||||
|
||||
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
|
||||
beforeAll(async () => {
|
||||
nextConfig.write(
|
||||
`
|
||||
const config = require('../next.config.js');
|
||||
module.exports = {
|
||||
...config,
|
||||
experimental: {
|
||||
useLightningcss: ${useLightningcss}
|
||||
}
|
||||
}`
|
||||
)
|
||||
})
|
||||
|
||||
beforeAll(async () => {
|
||||
await remove(join(appDir, '.next'))
|
||||
|
@ -150,11 +227,8 @@ describe('Nested @import() Global Support', () => {
|
|||
|
||||
expect(cssFiles.length).toBe(1)
|
||||
const cssContent = await readFile(join(cssFolder, cssFiles[0]), 'utf8')
|
||||
expect(
|
||||
cssContent.replace(/\/\*.*?\*\//g, '').trim()
|
||||
).toMatchInlineSnapshot(
|
||||
`".red-text{color:purple;font-weight:bolder;color:red}.blue-text{color:orange;font-weight:bolder;color:blue}"`
|
||||
)
|
||||
expect(cssContent.replace(/\/\*.*?\*\//g, '').trim()).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -163,6 +237,21 @@ describe('Nested @import() Global Support', () => {
|
|||
describe('Multi Global Support (reversed)', () => {
|
||||
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
|
||||
const appDir = join(fixturesDir, 'multi-global-reversed')
|
||||
const nextConfig = new File(join(appDir, 'next.config.js'))
|
||||
|
||||
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
|
||||
beforeAll(async () => {
|
||||
nextConfig.write(
|
||||
`
|
||||
const config = require('../next.config.js');
|
||||
module.exports = {
|
||||
...config,
|
||||
experimental: {
|
||||
useLightningcss: ${useLightningcss}
|
||||
}
|
||||
}`
|
||||
)
|
||||
})
|
||||
|
||||
beforeAll(async () => {
|
||||
await remove(join(appDir, '.next'))
|
||||
|
@ -184,9 +273,8 @@ describe('Multi Global Support (reversed)', () => {
|
|||
|
||||
expect(cssFiles.length).toBe(1)
|
||||
const cssContent = await readFile(join(cssFolder, cssFiles[0]), 'utf8')
|
||||
expect(
|
||||
cssContent.replace(/\/\*.*?\*\//g, '').trim()
|
||||
).toMatchInlineSnapshot(`".blue-text{color:blue}.red-text{color:red}"`)
|
||||
expect(cssContent.replace(/\/\*.*?\*\//g, '').trim()).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -194,6 +282,21 @@ describe('Multi Global Support (reversed)', () => {
|
|||
describe('CSS URL via `file-loader`', () => {
|
||||
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
|
||||
const appDir = join(fixturesDir, 'url-global')
|
||||
const nextConfig = new File(join(appDir, 'next.config.js'))
|
||||
|
||||
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
|
||||
beforeAll(async () => {
|
||||
nextConfig.write(
|
||||
`
|
||||
const config = require('../next.config.js');
|
||||
module.exports = {
|
||||
...config,
|
||||
experimental: {
|
||||
useLightningcss: ${useLightningcss}
|
||||
}
|
||||
}`
|
||||
)
|
||||
})
|
||||
|
||||
beforeAll(async () => {
|
||||
await remove(join(appDir, '.next'))
|
||||
|
@ -216,9 +319,7 @@ describe('CSS URL via `file-loader`', () => {
|
|||
|
||||
expect(cssFiles.length).toBe(1)
|
||||
const cssContent = await readFile(join(cssFolder, cssFiles[0]), 'utf8')
|
||||
expect(cssContent.replace(/\/\*.*?\*\//g, '').trim()).toMatch(
|
||||
/^\.red-text\{color:red;background-image:url\(\/_next\/static\/media\/dark\.[a-f0-9]{8}\.svg\) url\(\/_next\/static\/media\/dark2\.[a-f0-9]{8}\.svg\)\}\.blue-text\{color:orange;font-weight:bolder;background-image:url\(\/_next\/static\/media\/light\.[a-f0-9]{8}\.svg\);color:blue\}$/
|
||||
)
|
||||
expect(cssContent.replace(/\/\*.*?\*\//g, '').trim()).toMatchSnapshot()
|
||||
|
||||
const mediaFiles = await readdir(mediaFolder)
|
||||
expect(mediaFiles.length).toBe(3)
|
||||
|
@ -238,6 +339,7 @@ describe('CSS URL via `file-loader`', () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('CSS URL via `file-loader` and asset prefix (1)', () => {
|
||||
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
|
||||
|
|
|
@ -3,6 +3,7 @@ import cheerio from 'cheerio'
|
|||
import { readdir, readFile, remove } from 'fs-extra'
|
||||
import {
|
||||
findPort,
|
||||
File,
|
||||
killApp,
|
||||
nextBuild,
|
||||
nextStart,
|
||||
|
@ -17,11 +18,30 @@ describe('CSS Support', () => {
|
|||
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
|
||||
describe('CSS Compilation and Prefixing', () => {
|
||||
const appDir = join(fixturesDir, 'compilation-and-prefixing')
|
||||
const nextConfig = new File(join(appDir, 'next.config.js'))
|
||||
|
||||
beforeAll(async () => {
|
||||
await remove(join(appDir, '.next'))
|
||||
})
|
||||
|
||||
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
|
||||
beforeAll(async () => {
|
||||
nextConfig.write(
|
||||
`
|
||||
const config = require('../next.config.js');
|
||||
module.exports = {
|
||||
...config,
|
||||
experimental: {
|
||||
useLightningcss: ${useLightningcss}
|
||||
}
|
||||
}`
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
nextConfig.delete()
|
||||
})
|
||||
|
||||
it('should compile successfully', async () => {
|
||||
const { code, stdout } = await nextBuild(appDir, [], {
|
||||
stdout: true,
|
||||
|
@ -37,15 +57,18 @@ describe('CSS Support', () => {
|
|||
const cssFiles = files.filter((f) => /\.css$/.test(f))
|
||||
|
||||
expect(cssFiles.length).toBe(1)
|
||||
const cssContent = await readFile(join(cssFolder, cssFiles[0]), 'utf8')
|
||||
const cssContent = await readFile(
|
||||
join(cssFolder, cssFiles[0]),
|
||||
'utf8'
|
||||
)
|
||||
expect(
|
||||
cssContent.replace(/\/\*.*?\*\//g, '').trim()
|
||||
).toMatchInlineSnapshot(
|
||||
`"@media (min-width:480px) and (max-width:767px){::placeholder{color:green}}.flex-parsing{flex:0 0 calc(50% - var(--vertical-gutter))}.transform-parsing{transform:translate3d(0,0)}.css-grid-shorthand{grid-column:span 2}.g-docs-sidenav .filter::-webkit-input-placeholder{opacity:80%}"`
|
||||
)
|
||||
).toMatchSnapshot()
|
||||
|
||||
// Contains a source map
|
||||
expect(cssContent).toMatch(/\/\*#\s*sourceMappingURL=(.+\.map)\s*\*\//)
|
||||
expect(cssContent).toMatch(
|
||||
/\/\*#\s*sourceMappingURL=(.+\.map)\s*\*\//
|
||||
)
|
||||
})
|
||||
|
||||
it(`should've emitted a source map`, async () => {
|
||||
|
@ -59,46 +82,34 @@ describe('CSS Support', () => {
|
|||
await readFile(join(cssFolder, cssMapFiles[0]), 'utf8')
|
||||
).trim()
|
||||
|
||||
const { version, mappings, sourcesContent } = JSON.parse(cssMapContent)
|
||||
expect({ version, mappings, sourcesContent }).toMatchInlineSnapshot(`
|
||||
{
|
||||
"mappings": "AAAA,+CACE,cACE,WACF,CACF,CAEA,cACE,2CACF,CAEA,mBACE,0BACF,CAEA,oBACE,kBACF,CAEA,mDACE,WACF",
|
||||
"sourcesContent": [
|
||||
"@media (480px <= width < 768px) {
|
||||
::placeholder {
|
||||
color: green;
|
||||
}
|
||||
}
|
||||
|
||||
.flex-parsing {
|
||||
flex: 0 0 calc(50% - var(--vertical-gutter));
|
||||
}
|
||||
|
||||
.transform-parsing {
|
||||
transform: translate3d(0px, 0px);
|
||||
}
|
||||
|
||||
.css-grid-shorthand {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.g-docs-sidenav .filter::-webkit-input-placeholder {
|
||||
opacity: 80%;
|
||||
}
|
||||
",
|
||||
],
|
||||
"version": 3,
|
||||
}
|
||||
`)
|
||||
const { version, mappings, sourcesContent } =
|
||||
JSON.parse(cssMapContent)
|
||||
expect({ version, mappings, sourcesContent }).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('React Lifecyce Order (production)', () => {
|
||||
const appDir = join(fixturesDir, 'transition-react')
|
||||
const nextConfig = new File(join(appDir, 'next.config.js'))
|
||||
beforeAll(async () => {
|
||||
await remove(join(appDir, '.next'))
|
||||
})
|
||||
|
||||
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
|
||||
beforeAll(async () => {
|
||||
nextConfig.write(
|
||||
`
|
||||
const config = require('../next.config.js');
|
||||
module.exports = {
|
||||
...config,
|
||||
experimental: {
|
||||
useLightningcss: ${useLightningcss}
|
||||
}
|
||||
}`
|
||||
)
|
||||
})
|
||||
|
||||
let appPort
|
||||
let app
|
||||
let code
|
||||
|
@ -135,9 +146,25 @@ describe('CSS Support', () => {
|
|||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Has CSS in computed styles in Production', () => {
|
||||
const appDir = join(fixturesDir, 'multi-page')
|
||||
const nextConfig = new File(join(appDir, 'next.config.js'))
|
||||
|
||||
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
|
||||
beforeAll(async () => {
|
||||
nextConfig.write(
|
||||
`
|
||||
const config = require('../next.config.js');
|
||||
module.exports = {
|
||||
...config,
|
||||
experimental: {
|
||||
useLightningcss: ${useLightningcss}
|
||||
}
|
||||
}`
|
||||
)
|
||||
})
|
||||
|
||||
let appPort
|
||||
let app
|
||||
|
@ -181,7 +208,9 @@ describe('CSS Support', () => {
|
|||
|
||||
const cssSheet = $('link[rel="stylesheet"]')
|
||||
expect(cssSheet.length).toBe(1)
|
||||
expect(cssSheet.attr('href')).toMatch(/^\/_next\/static\/css\/.*\.css$/)
|
||||
expect(cssSheet.attr('href')).toMatch(
|
||||
/^\/_next\/static\/css\/.*\.css$/
|
||||
)
|
||||
|
||||
/* ensure CSS preloaded first */
|
||||
const allPreloads = [].slice.call($('link[rel="preload"]'))
|
||||
|
@ -191,9 +220,25 @@ describe('CSS Support', () => {
|
|||
expect(styleIndexes).toEqual([0])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Good CSS Import from node_modules', () => {
|
||||
const appDir = join(fixturesDir, 'npm-import')
|
||||
const nextConfig = new File(join(appDir, 'next.config.js'))
|
||||
|
||||
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
|
||||
beforeAll(async () => {
|
||||
nextConfig.write(
|
||||
`
|
||||
const config = require('../next.config.js');
|
||||
module.exports = {
|
||||
...config,
|
||||
experimental: {
|
||||
useLightningcss: ${useLightningcss}
|
||||
}
|
||||
}`
|
||||
)
|
||||
})
|
||||
|
||||
beforeAll(async () => {
|
||||
await remove(join(appDir, '.next'))
|
||||
|
@ -214,15 +259,34 @@ describe('CSS Support', () => {
|
|||
const cssFiles = files.filter((f) => /\.css$/.test(f))
|
||||
|
||||
expect(cssFiles.length).toBe(1)
|
||||
const cssContent = await readFile(join(cssFolder, cssFiles[0]), 'utf8')
|
||||
const cssContent = await readFile(
|
||||
join(cssFolder, cssFiles[0]),
|
||||
'utf8'
|
||||
)
|
||||
expect(cssContent.replace(/\/\*.*?\*\//g, '').trim()).toMatch(
|
||||
/nprogress/
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Good Nested CSS Import from node_modules', () => {
|
||||
const appDir = join(fixturesDir, 'npm-import-nested')
|
||||
const nextConfig = new File(join(appDir, 'next.config.js'))
|
||||
|
||||
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
|
||||
beforeAll(async () => {
|
||||
nextConfig.write(
|
||||
`
|
||||
const config = require('../next.config.js');
|
||||
module.exports = {
|
||||
...config,
|
||||
experimental: {
|
||||
useLightningcss: ${useLightningcss}
|
||||
}
|
||||
}`
|
||||
)
|
||||
})
|
||||
|
||||
beforeAll(async () => {
|
||||
await remove(join(appDir, '.next'))
|
||||
|
@ -243,10 +307,14 @@ describe('CSS Support', () => {
|
|||
const cssFiles = files.filter((f) => /\.css$/.test(f))
|
||||
|
||||
expect(cssFiles.length).toBe(1)
|
||||
const cssContent = await readFile(join(cssFolder, cssFiles[0]), 'utf8')
|
||||
const cssContent = await readFile(
|
||||
join(cssFolder, cssFiles[0]),
|
||||
'utf8'
|
||||
)
|
||||
expect(
|
||||
cssContent.replace(/\/\*.*?\*\//g, '').trim()
|
||||
).toMatchInlineSnapshot(`".other{color:blue}.test{color:red}"`)
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -256,6 +324,21 @@ describe('CSS Support', () => {
|
|||
describe('CSS Property Ordering', () => {
|
||||
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
|
||||
const appDir = join(fixturesDir, 'next-issue-15468')
|
||||
const nextConfig = new File(join(appDir, 'next.config.js'))
|
||||
|
||||
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
|
||||
beforeAll(async () => {
|
||||
nextConfig.write(
|
||||
`
|
||||
const config = require('../next.config.js');
|
||||
module.exports = {
|
||||
...config,
|
||||
experimental: {
|
||||
useLightningcss: ${useLightningcss}
|
||||
}
|
||||
}`
|
||||
)
|
||||
})
|
||||
|
||||
let appPort
|
||||
let app
|
||||
|
@ -293,3 +376,4 @@ describe('CSS Property Ordering', () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -20,6 +20,21 @@ const fixturesDir = join(__dirname, '../..', 'css-fixtures')
|
|||
// https://github.com/vercel/next.js/issues/12343
|
||||
describe('Basic CSS Modules Ordering', () => {
|
||||
const appDir = join(fixturesDir, 'next-issue-12343')
|
||||
const nextConfig = new File(join(appDir, 'next.config.js'))
|
||||
|
||||
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
|
||||
beforeAll(async () => {
|
||||
nextConfig.write(
|
||||
`
|
||||
const config = require('../next.config.js');
|
||||
module.exports = {
|
||||
...config,
|
||||
experimental: {
|
||||
useLightningcss: ${useLightningcss}
|
||||
}
|
||||
}`
|
||||
)
|
||||
})
|
||||
let app, appPort
|
||||
|
||||
function tests() {
|
||||
|
@ -78,7 +93,10 @@ describe('Basic CSS Modules Ordering', () => {
|
|||
})
|
||||
}
|
||||
|
||||
describe('Development Mode', () => {
|
||||
;(process.env.TURBOPACK ? describe.skip : describe)(
|
||||
'Development Mode',
|
||||
() => {
|
||||
// TODO(PACK-2308): Fix the ordering issue of CSS Modules in turbopack
|
||||
beforeAll(async () => {
|
||||
await remove(join(appDir, '.next'))
|
||||
})
|
||||
|
@ -91,8 +109,11 @@ describe('Basic CSS Modules Ordering', () => {
|
|||
})
|
||||
|
||||
tests()
|
||||
})
|
||||
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
|
||||
}
|
||||
)
|
||||
;(process.env.TURBOPACK ? describe.skip : describe)(
|
||||
'production mode',
|
||||
() => {
|
||||
beforeAll(async () => {
|
||||
await remove(join(appDir, '.next'))
|
||||
})
|
||||
|
@ -106,6 +127,8 @@ describe('Basic CSS Modules Ordering', () => {
|
|||
})
|
||||
|
||||
tests()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -170,6 +193,21 @@ describe('Data URLs', () => {
|
|||
|
||||
describe('Ordering with Global CSS and Modules (dev)', () => {
|
||||
const appDir = join(fixturesDir, 'global-and-module-ordering')
|
||||
const nextConfig = new File(join(appDir, 'next.config.js'))
|
||||
|
||||
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
|
||||
beforeAll(async () => {
|
||||
nextConfig.write(
|
||||
`
|
||||
const config = require('../next.config.js');
|
||||
module.exports = {
|
||||
...config,
|
||||
experimental: {
|
||||
useLightningcss: ${useLightningcss}
|
||||
}
|
||||
}`
|
||||
)
|
||||
})
|
||||
|
||||
let appPort
|
||||
let app
|
||||
|
@ -250,10 +288,26 @@ describe('Ordering with Global CSS and Modules (dev)', () => {
|
|||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Ordering with Global CSS and Modules (prod)', () => {
|
||||
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
|
||||
const appDir = join(fixturesDir, 'global-and-module-ordering')
|
||||
const nextConfig = new File(join(appDir, 'next.config.js'))
|
||||
|
||||
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
|
||||
beforeAll(async () => {
|
||||
nextConfig.write(
|
||||
`
|
||||
const config = require('../next.config.js');
|
||||
module.exports = {
|
||||
...config,
|
||||
experimental: {
|
||||
useLightningcss: ${useLightningcss}
|
||||
}
|
||||
}`
|
||||
)
|
||||
})
|
||||
|
||||
let appPort
|
||||
let app
|
||||
|
@ -286,6 +340,7 @@ describe('Ordering with Global CSS and Modules (prod)', () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// https://github.com/vercel/next.js/issues/12445
|
||||
// This feature is not supported in Turbopack
|
||||
|
@ -293,6 +348,21 @@ describe('Ordering with Global CSS and Modules (prod)', () => {
|
|||
'CSS Modules Composes Ordering',
|
||||
() => {
|
||||
const appDir = join(fixturesDir, 'composes-ordering')
|
||||
const nextConfig = new File(join(appDir, 'next.config.js'))
|
||||
|
||||
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
|
||||
beforeAll(async () => {
|
||||
nextConfig.write(
|
||||
`
|
||||
const config = require('../next.config.js');
|
||||
module.exports = {
|
||||
...config,
|
||||
experimental: {
|
||||
useLightningcss: ${useLightningcss}
|
||||
}
|
||||
}`
|
||||
)
|
||||
})
|
||||
let app, appPort
|
||||
|
||||
function tests(isDev = false) {
|
||||
|
@ -461,5 +531,6 @@ describe('Ordering with Global CSS and Modules (prod)', () => {
|
|||
tests()
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
|
|
@ -3,6 +3,7 @@ import { pathExists, readFile, readJSON, remove } from 'fs-extra'
|
|||
import {
|
||||
check,
|
||||
findPort,
|
||||
File,
|
||||
killApp,
|
||||
nextBuild,
|
||||
nextStart,
|
||||
|
@ -17,7 +18,21 @@ describe('CSS Support', () => {
|
|||
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
|
||||
describe('CSS Import from node_modules', () => {
|
||||
const appDir = join(fixturesDir, 'npm-import-bad')
|
||||
const nextConfig = new File(join(appDir, 'next.config.js'))
|
||||
|
||||
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
|
||||
beforeAll(async () => {
|
||||
nextConfig.write(
|
||||
`
|
||||
const config = require('../next.config.js');
|
||||
module.exports = {
|
||||
...config,
|
||||
experimental: {
|
||||
useLightningcss: ${useLightningcss}
|
||||
}
|
||||
}`
|
||||
)
|
||||
})
|
||||
beforeAll(async () => {
|
||||
await remove(join(appDir, '.next'))
|
||||
})
|
||||
|
@ -31,6 +46,7 @@ describe('CSS Support', () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// https://github.com/vercel/next.js/issues/18557
|
||||
describe('CSS page transition inject <style> with nonce so it works with CSP header', () => {
|
||||
|
@ -191,6 +207,22 @@ describe('CSS Support', () => {
|
|||
|
||||
describe('CSS Cleanup on Render Failure', () => {
|
||||
const appDir = join(fixturesDir, 'transition-cleanup')
|
||||
const nextConfig = new File(join(appDir, 'next.config.js'))
|
||||
|
||||
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
|
||||
beforeAll(async () => {
|
||||
nextConfig.write(
|
||||
`
|
||||
const config = require('../next.config.js');
|
||||
module.exports = {
|
||||
...config,
|
||||
experimental: {
|
||||
useLightningcss: ${useLightningcss}
|
||||
}
|
||||
}`
|
||||
)
|
||||
})
|
||||
|
||||
let app, appPort
|
||||
|
||||
function tests() {
|
||||
|
@ -254,9 +286,25 @@ describe('CSS Support', () => {
|
|||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Page reload on CSS missing', () => {
|
||||
const appDir = join(fixturesDir, 'transition-reload')
|
||||
const nextConfig = new File(join(appDir, 'next.config.js'))
|
||||
|
||||
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
|
||||
beforeAll(async () => {
|
||||
nextConfig.write(
|
||||
`
|
||||
const config = require('../next.config.js');
|
||||
module.exports = {
|
||||
...config,
|
||||
experimental: {
|
||||
useLightningcss: ${useLightningcss}
|
||||
}
|
||||
}`
|
||||
)
|
||||
})
|
||||
let app, appPort
|
||||
|
||||
function tests() {
|
||||
|
@ -306,7 +354,9 @@ describe('CSS Support', () => {
|
|||
e.endsWith('.css')
|
||||
)
|
||||
if (files.length < 1) throw new Error()
|
||||
await Promise.all(files.map((f) => remove(join(appDir, '.next', f))))
|
||||
await Promise.all(
|
||||
files.map((f) => remove(join(appDir, '.next', f)))
|
||||
)
|
||||
})
|
||||
afterAll(async () => {
|
||||
await killApp(app)
|
||||
|
@ -316,9 +366,26 @@ describe('CSS Support', () => {
|
|||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Page hydrates with CSS and not waiting on dependencies', () => {
|
||||
const appDir = join(fixturesDir, 'hydrate-without-deps')
|
||||
const nextConfig = new File(join(appDir, 'next.config.js'))
|
||||
|
||||
describe.each([true, false])(`useLightnincsss(%s)`, (useLightningcss) => {
|
||||
beforeAll(async () => {
|
||||
nextConfig.write(
|
||||
`
|
||||
const config = require('../next.config.js');
|
||||
module.exports = {
|
||||
...config,
|
||||
experimental: {
|
||||
useLightningcss: ${useLightningcss}
|
||||
}
|
||||
}`
|
||||
)
|
||||
})
|
||||
|
||||
let app, appPort
|
||||
|
||||
function tests() {
|
||||
|
@ -417,3 +484,4 @@ describe('CSS Support', () => {
|
|||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue