diff --git a/.changeset/wise-oranges-talk.md b/.changeset/wise-oranges-talk.md new file mode 100644 index 000000000..68dc3cf31 --- /dev/null +++ b/.changeset/wise-oranges-talk.md @@ -0,0 +1,6 @@ +--- +"@vanilla-extract/next-plugin": patch +"@vanilla-extract/webpack-plugin": patch +--- + +Fixes Next.js 13 CSS output on Windows when using React Server Components diff --git a/.gitignore b/.gitignore index 6b6f822c9..8d1c05a7f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ test-results .pnp.* .next -.DS_Store \ No newline at end of file +.DS_Store +.idea diff --git a/packages/next-plugin/package.json b/packages/next-plugin/package.json index ef2918072..2dabeceb6 100644 --- a/packages/next-plugin/package.json +++ b/packages/next-plugin/package.json @@ -4,6 +4,11 @@ "description": "Zero-runtime Stylesheets-in-TypeScript", "main": "dist/vanilla-extract-next-plugin.cjs.js", "module": "dist/vanilla-extract-next-plugin.esm.js", + "preconstruct": { + "entrypoints": [ + "index.ts" + ] + }, "files": [ "/dist" ], @@ -15,8 +20,7 @@ "author": "SEEK", "license": "MIT", "dependencies": { - "@vanilla-extract/webpack-plugin": "^2.3.0", - "browserslist": "^4.19.1" + "@vanilla-extract/webpack-plugin": "^2.3.0" }, "peerDependencies": { "next": ">=12.1.7" diff --git a/packages/next-plugin/src/index.ts b/packages/next-plugin/src/index.ts index e41d3ae93..3a67ac522 100644 --- a/packages/next-plugin/src/index.ts +++ b/packages/next-plugin/src/index.ts @@ -1,40 +1,45 @@ -import { VanillaExtractPlugin } from '@vanilla-extract/webpack-plugin'; -import browserslist from 'browserslist'; -import { lazyPostCSS } from 'next/dist/build/webpack/config/blocks/css'; +// @ts-expect-error +import browserslist from 'next/dist/compiled/browserslist'; +import NextMiniCssExtractPluginDefault from 'next/dist/build/webpack/plugins/mini-css-extract-plugin'; +import { VanillaExtractPlugin } from '@vanilla-extract/webpack-plugin/next'; import { findPagesDir } from 'next/dist/lib/find-pages-dir'; +import { lazyPostCSS } from 'next/dist/build/webpack/config/blocks/css'; import { cssFileResolve } from 'next/dist/build/webpack/config/blocks/css/loaders/file-resolve'; -import NextMiniCssExtractPluginDefault from 'next/dist/build/webpack/plugins/mini-css-extract-plugin'; import type webpack from 'webpack'; -import type { NextConfig } from 'next/types'; -import type { WebpackConfigContext } from 'next/dist/server/config-shared'; +import type { + NextConfig, + WebpackConfigContext, +} from 'next/dist/server/config-shared'; + +type PluginOptions = ConstructorParameters[0]; const NextMiniCssExtractPlugin = NextMiniCssExtractPluginDefault as any; -function getSupportedBrowsers(dir: any, isDevelopment: any) { - let browsers; +// Adopted from https://github.com/vercel/next.js/blob/1f1632979c78b3edfe59fd85d8cce62efcdee688/packages/next/build/webpack-config.ts#L60-L72 +function getSupportedBrowsers(dir: string, isDevelopment: boolean) { try { - browsers = browserslist.loadConfig({ + return browserslist.loadConfig({ path: dir, env: isDevelopment ? 'development' : 'production', }); - } catch {} - - return browsers; + } catch (_) { + return undefined; + } } -type PluginOptions = ConstructorParameters[0]; - -// https://github.com/vercel/next.js/blob/canary/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts#L7 +// Adopt from Next.js' getGlobalCssLoader +// https://github.com/vercel/next.js/blob/6e5b935fd7a61497f6854a81aec7df3a5dbf61ac/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts#L7 const getVanillaExtractCssLoaders = ( options: WebpackConfigContext, assetPrefix: string, ) => { const loaders: webpack.RuleSetUseItem[] = []; - // https://github.com/vercel/next.js/blob/a4f2bbbe2047d4ed88e9b6f32f6b0adfc8d0c46a/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts#L14 + // Adopt from Next.js' getClientStyleLoader + // https://github.com/vercel/next.js/blob/6e5b935fd7a61497f6854a81aec7df3a5dbf61ac/packages/next/src/build/webpack/config/blocks/css/loaders/client.ts#L3 if (!options.isServer) { - // https://github.com/vercel/next.js/blob/a4f2bbbe2047d4ed88e9b6f32f6b0adfc8d0c46a/packages/next/src/build/webpack/config/blocks/css/loaders/client.ts#L44 + // https://github.com/vercel/next.js/blob/6e5b935fd7a61497f6854a81aec7df3a5dbf61ac/packages/next/src/build/webpack/config/blocks/css/loaders/client.ts#L44 // next-style-loader will mess up css order in development mode. // Next.js appDir doesn't use next-style-loader either. // So we always use css-loader here, to simplify things and get proper order of output CSS @@ -54,7 +59,7 @@ const getVanillaExtractCssLoaders = ( undefined, ); - // https://github.com/vercel/next.js/blob/a4f2bbbe2047d4ed88e9b6f32f6b0adfc8d0c46a/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts#L28 + // https://github.com/vercel/next.js/blob/6e5b935fd7a61497f6854a81aec7df3a5dbf61ac/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts#L28 loaders.push({ loader: require.resolve('next/dist/build/webpack/loaders/css-loader/src'), options: { @@ -76,7 +81,7 @@ const getVanillaExtractCssLoaders = ( }, }); - // https://github.com/vercel/next.js/blob/a4f2bbbe2047d4ed88e9b6f32f6b0adfc8d0c46a/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts#L43 + // https://github.com/vercel/next.js/blob/6e5b935fd7a61497f6854a81aec7df3a5dbf61ac/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts#L29-L38 loaders.push({ loader: require.resolve( 'next/dist/build/webpack/loaders/postcss-loader/src', @@ -86,102 +91,109 @@ const getVanillaExtractCssLoaders = ( }, }); + // https://github.com/SukkaW/style9-webpack/blob/f51c46bbcd95ea3b988d3559c3b35cc056874366/src/next-appdir/index.ts#L103-L105 + loaders.push({ + loader: VanillaExtractPlugin.loader, + }); + return loaders; }; -export const createVanillaExtractPlugin = - (pluginOptions: PluginOptions = {}) => - (nextConfig: NextConfig = {}): NextConfig => - Object.assign({}, nextConfig, { - webpack(config: any, options: WebpackConfigContext) { - const { dir, dev, isServer, config: resolvedNextConfig } = options; - const findPagesDirResult = findPagesDir( - dir, - resolvedNextConfig.experimental?.appDir, - ); - - // https://github.com/vercel/next.js/blob/1fb4cad2a8329811b5ccde47217b4a6ae739124e/packages/next/build/index.ts#L336 - // https://github.com/vercel/next.js/blob/1fb4cad2a8329811b5ccde47217b4a6ae739124e/packages/next/build/webpack-config.ts#L626 - // https://github.com/vercel/next.js/pull/43916 - const hasAppDir = - // on Next.js 12, findPagesDirResult is a string. on Next.js 13, findPagesDirResult is an object - !!resolvedNextConfig.experimental?.appDir && - !!(findPagesDirResult && findPagesDirResult.appDir); - - const outputCss = hasAppDir - ? // Always output css since Next.js App Router needs to collect Server CSS from React Server Components - true - : // There is no appDir, do not output css on server build - !isServer; - - const cssRules = config.module.rules.find( - (rule: any) => - Array.isArray(rule.oneOf) && - rule.oneOf.some( - ({ test }: any) => - typeof test === 'object' && - typeof test.test === 'function' && - test.test('filename.css'), - ), - ).oneOf; - - cssRules.unshift({ - test: /\.vanilla\.css$/i, - sideEffects: true, - use: getVanillaExtractCssLoaders( - options, - resolvedNextConfig.assetPrefix, +export const createVanillaExtractPlugin = ( + pluginOptions: PluginOptions = {}, +) => { + return (nextConfig: NextConfig = {}): NextConfig => ({ + ...nextConfig, + webpack(config: any, options: WebpackConfigContext) { + const { dir, dev, isServer, config: resolvedNextConfig } = options; + + // https://github.com/vercel/next.js/blob/1fb4cad2a8329811b5ccde47217b4a6ae739124e/packages/next/build/index.ts#L336 + // https://github.com/vercel/next.js/blob/1fb4cad2a8329811b5ccde47217b4a6ae739124e/packages/next/build/webpack-config.ts#L626 + // https://github.com/vercel/next.js/pull/43916 + // on Next.js 12, findPagesDirResult is a string. on Next.js 13, findPagesDirResult is an object + const findPagesDirResult = findPagesDir( + dir, + resolvedNextConfig.experimental?.appDir ?? false, + ); + const hasAppDir = + !!resolvedNextConfig.experimental?.appDir && + !!(findPagesDirResult && findPagesDirResult.appDir); + + const outputCss = hasAppDir + ? // Always output css since Next.js App Router needs to collect Server CSS from React Server Components + true + : // There is no appDir, do not output css on server build + !isServer; + + // https://github.com/vercel/next.js/blob/6e5b935fd7a61497f6854a81aec7df3a5dbf61ac/packages/next/src/build/webpack/config/helpers.ts#L12-L21 + const cssRules = config.module.rules.find( + (rule: any) => + Array.isArray(rule.oneOf) && + rule.oneOf.some( + ({ test }: any) => + typeof test === 'object' && + typeof test.test === 'function' && + test.test('filename.css'), ), - }); - - // vanilla-extract need to emit the css file on both server and client, both during the - // development and production. - // However, Next.js only add MiniCssExtractPlugin on pages dir + client build + production mode. - // - // To simplify the logic at our side, we will add MiniCssExtractPlugin based on - // the "instanceof" check (We will only add our required MiniCssExtractPlugin if - // Next.js hasn't added it yet). - // This also prevent multiple MiniCssExtractPlugin being added (which will cause - // RealContentHashPlugin to panic) - if ( - !config.plugins.some( - (plugin: any) => plugin instanceof NextMiniCssExtractPlugin, - ) - ) { - // HMR reloads the CSS file when the content changes but does not use - // the new file name, which means it can't contain a hash. - const filename = dev - ? 'static/css/[name].css' - : 'static/css/[contenthash].css'; - - config.plugins.push( - new NextMiniCssExtractPlugin({ - filename, - chunkFilename: filename, - // Next.js guarantees that CSS order "doesn't matter", due to imposed - // restrictions: - // 1. Global CSS can only be defined in a single entrypoint (_app) - // 2. CSS Modules generate scoped class names by default and cannot - // include Global CSS (:global() selector). - // - // While not a perfect guarantee (e.g. liberal use of `:global()` - // selector), this assumption is required to code-split CSS. - // - // If this warning were to trigger, it'd be unactionable by the user, - // but likely not valid -- so just disable it. - ignoreOrder: true, - }), - ); - } + ).oneOf; + + // https://github.com/SukkaW/style9-webpack/blob/f51c46bbcd95ea3b988d3559c3b35cc056874366/src/next-appdir/index.ts#L187-L190 + cssRules.unshift({ + test: /vanilla\.virtual\.css/i, + sideEffects: true, + use: getVanillaExtractCssLoaders( + options, + resolvedNextConfig.assetPrefix, + ), + }); + + // vanilla-extract need to emit the css file on both server and client, both during the + // development and production. + // However, Next.js only add MiniCssExtractPlugin on pages dir + client build + production mode. + // + // To simplify the logic at our side, we will add MiniCssExtractPlugin based on + // the "instanceof" check (We will only add our required MiniCssExtractPlugin if + // Next.js hasn't added it yet). + // This also prevent multiple MiniCssExtractPlugin being added (which will cause + // RealContentHashPlugin to panic) + if ( + !config.plugins.some((p: any) => p instanceof NextMiniCssExtractPlugin) + ) { + // HMR reloads the CSS file when the content changes but does not use + // the new file name, which means it can't contain a hash. + const filename = dev + ? 'static/css/[name].css' + : 'static/css/[contenthash].css'; config.plugins.push( - new VanillaExtractPlugin({ outputCss, ...pluginOptions }), + new NextMiniCssExtractPlugin({ + filename, + chunkFilename: filename, + // Next.js guarantees that CSS order "doesn't matter", due to imposed + // restrictions: + // 1. Global CSS can only be defined in a single entrypoint (_app) + // 2. CSS Modules generate scoped class names by default and cannot + // include Global CSS (:global() selector). + // + // While not a perfect guarantee (e.g. liberal use of `:global()` + // selector), this assumption is required to code-split CSS. + // + // If this warning were to trigger, it'd be unactionable by the user, + // but likely not valid -- so just disable it. + ignoreOrder: true, + }), ); + } - if (typeof nextConfig.webpack === 'function') { - return nextConfig.webpack(config, options); - } + config.plugins.push( + new VanillaExtractPlugin({ outputCss, ...pluginOptions }), + ); - return config; - }, - }); + if (typeof nextConfig.webpack === 'function') { + return nextConfig.webpack(config, options); + } + + return config; + }, + }); +}; diff --git a/packages/webpack-plugin/next/package.json b/packages/webpack-plugin/next/package.json new file mode 100644 index 000000000..85049ed95 --- /dev/null +++ b/packages/webpack-plugin/next/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/vanilla-extract-webpack-plugin-next.cjs.js", + "module": "dist/vanilla-extract-webpack-plugin-next.esm.js" +} diff --git a/packages/webpack-plugin/package.json b/packages/webpack-plugin/package.json index 015e9b13c..39c09ff8f 100644 --- a/packages/webpack-plugin/package.json +++ b/packages/webpack-plugin/package.json @@ -17,20 +17,33 @@ "./virtualFileLoader": { "module": "./virtualFileLoader/dist/vanilla-extract-webpack-plugin-virtualFileLoader.esm.js", "default": "./virtualFileLoader/dist/vanilla-extract-webpack-plugin-virtualFileLoader.cjs.js" + }, + "./next": { + "module": "./next/dist/vanilla-extract-webpack-plugin-next.esm.js", + "default": "./next/dist/vanilla-extract-webpack-plugin-next.cjs.js" + }, + "./virtualNextFileLoader": { + "module": "./virtualNextFileLoader/dist/vanilla-extract-webpack-plugin-virtualNextFileLoader.esm.js", + "default": "./virtualNextFileLoader/dist/vanilla-extract-webpack-plugin-virtualNextFileLoader.cjs.js" } }, "preconstruct": { "entrypoints": [ "index.ts", "loader.ts", - "virtualFileLoader.ts" + "virtualFileLoader.ts", + "next.ts", + "virtualNextFileLoader.ts" ] }, "files": [ "/dist", "/loader", "/virtualFileLoader", - "extracted.js" + "/next", + "/virtualNextFileLoader", + "extracted.js", + "vanilla.virtual.css" ], "repository": { "type": "git", diff --git a/packages/webpack-plugin/src/index.ts b/packages/webpack-plugin/src/index.ts index beebad067..059a62418 100644 --- a/packages/webpack-plugin/src/index.ts +++ b/packages/webpack-plugin/src/index.ts @@ -1,103 +1,8 @@ -import { cssFileFilter, IdentifierOption } from '@vanilla-extract/integration'; -import type { Compiler, RuleSetRule } from 'webpack'; - -import { ChildCompiler } from './compiler'; -import createCompat, { WebpackCompat } from './compat'; - -const pluginName = 'VanillaExtractPlugin'; - -function markCSSFilesAsSideEffects(compiler: Compiler, compat: WebpackCompat) { - compiler.hooks.normalModuleFactory.tap(pluginName, (nmf) => { - if (compat.isWebpack5) { - nmf.hooks.createModule.tap( - pluginName, - // @ts-expect-error CreateData is typed as 'object'... - (createData: { - matchResource?: string; - settings: { sideEffects?: boolean }; - }) => { - if ( - createData.matchResource && - createData.matchResource.endsWith('.vanilla.css') - ) { - createData.settings.sideEffects = true; - } - }, - ); - } else { - nmf.hooks.afterResolve.tap( - pluginName, - // @ts-expect-error Can't be typesafe for webpack 4 - (result: { - matchResource?: string; - settings: { sideEffects?: boolean }; - }) => { - if ( - result.matchResource && - result.matchResource.endsWith('.vanilla.css') - ) { - result.settings.sideEffects = true; - } - }, - ); - } - }); -} - -interface PluginOptions { - test?: RuleSetRule['test']; - identifiers?: IdentifierOption; - outputCss?: boolean; - externals?: any; - /** @deprecated */ - allowRuntime?: boolean; -} -export class VanillaExtractPlugin { - test: RuleSetRule['test']; - outputCss: boolean; - allowRuntime: boolean; - childCompiler: ChildCompiler; - identifiers?: IdentifierOption; - - constructor(options: PluginOptions = {}) { - const { - test = cssFileFilter, - outputCss = true, - externals, - allowRuntime, - identifiers, - } = options; - - if (allowRuntime !== undefined) { - console.warn('The "allowRuntime" option is deprecated.'); - } - - this.test = test; - this.outputCss = outputCss; - this.allowRuntime = allowRuntime ?? false; - this.childCompiler = new ChildCompiler(externals); - this.identifiers = identifiers; - } +import type { Compiler } from 'webpack'; +import { AbstractVanillaExtractPlugin } from './plugin'; +export class VanillaExtractPlugin extends AbstractVanillaExtractPlugin { apply(compiler: Compiler) { - const compat = createCompat( - Boolean(compiler.webpack && compiler.webpack.version), - ); - - markCSSFilesAsSideEffects(compiler, compat); - - compiler.options.module?.rules.splice(0, 0, { - test: this.test, - use: [ - { - loader: require.resolve('../loader'), - options: { - outputCss: this.outputCss, - childCompiler: this.childCompiler, - identifiers: this.identifiers, - }, - }, - ], - }); + this.inject(compiler, 'virtualFileLoader'); } } diff --git a/packages/webpack-plugin/src/loader.ts b/packages/webpack-plugin/src/loader.ts index 0812b6838..08fb9c287 100644 --- a/packages/webpack-plugin/src/loader.ts +++ b/packages/webpack-plugin/src/loader.ts @@ -2,29 +2,34 @@ import path from 'path'; // @ts-expect-error import loaderUtils from 'loader-utils'; import { + getPackageInfo, IdentifierOption, processVanillaFile, - transform, serializeCss, - getPackageInfo, + transform, } from '@vanilla-extract/integration'; import type { LoaderContext } from './types'; import { debug, formatResourcePath } from './logger'; import { ChildCompiler } from './compiler'; -const virtualLoader = require.resolve( +const virtualFileLoader = require.resolve( path.join( path.dirname(require.resolve('../../package.json')), 'virtualFileLoader', ), ); -const emptyCssExtractionFile = path.join( +const virtualFileLoaderExtractionFile = path.join( path.dirname(require.resolve('../../package.json')), 'extracted.js', ); +const virtualNextFileLoaderExtractionFile = path.join( + path.dirname(require.resolve('../../package.json')), + 'vanilla.virtual.css', +); + interface LoaderOptions { outputCss: boolean; identifiers?: IdentifierOption; @@ -32,6 +37,7 @@ interface LoaderOptions { interface InternalLoaderOptions extends LoaderOptions { childCompiler: ChildCompiler; + virtualLoader: 'virtualFileLoader' | 'virtualNextFileLoader'; } const defaultIdentifierOption = ( @@ -63,9 +69,8 @@ export default function (this: LoaderContext, source: string) { } export function pitch(this: LoaderContext) { - const { childCompiler, outputCss, identifiers } = loaderUtils.getOptions( - this, - ) as InternalLoaderOptions; + const { childCompiler, outputCss, identifiers, virtualLoader } = + loaderUtils.getOptions(this) as InternalLoaderOptions; const log = debug( `vanilla-extract:loader:${formatResourcePath(this.resourcePath)}`, @@ -97,17 +102,35 @@ export function pitch(this: LoaderContext) { identOption: defaultIdentifierOption(this.mode, identifiers), serializeVirtualCssPath: async ({ fileName, source }) => { const serializedCss = await serializeCss(source); - const virtualResourceLoader = `${virtualLoader}?${JSON.stringify({ - fileName, - source: serializedCss, - })}`; - - const request = loaderUtils.stringifyRequest( - this, - `${fileName}!=!${virtualResourceLoader}!${emptyCssExtractionFile}`, - ); - return `import ${request}`; + if (virtualLoader === 'virtualFileLoader') { + const virtualResourceLoader = `${virtualFileLoader}?${JSON.stringify( + { + fileName, + source: serializedCss, + }, + )}`; + const request = loaderUtils.stringifyRequest( + this, + `${fileName}!=!${virtualResourceLoader}!${virtualFileLoaderExtractionFile}`, + ); + return `import ${request}`; + } else { + // https://github.com/SukkaW/style9-webpack/blob/f51c46bbcd95ea3b988d3559c3b35cc056874366/src/next-appdir/style9-next-loader.ts#L64-L72 + const request = loaderUtils.stringifyRequest( + this, + // Next.js RSC CSS extraction will discard any loaders in the request. + // So we need to pass virtual css information through resourceQuery. + // https://github.com/vercel/next.js/blob/3a9bfe60d228fc2fd8fe65b76d49a0d21df4ecc7/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts#L425-L429 + // The compressed serialized CSS of vanilla-extract will add compressionFlag. + // Causing the resourceQuery to be abnormally split, so uri encoding is required. + // https://github.com/vanilla-extract-css/vanilla-extract/blob/58005eb5e7456cf2b3c04ea7aef29677db37cc3c/packages/integration/src/serialize.ts#L15 + `${virtualNextFileLoaderExtractionFile}?${encodeURIComponent( + JSON.stringify({ fileName, source: serializedCss }), + )}`, + ); + return `import ${request}`; + } }, }); diff --git a/packages/webpack-plugin/src/next.ts b/packages/webpack-plugin/src/next.ts new file mode 100644 index 000000000..193a24708 --- /dev/null +++ b/packages/webpack-plugin/src/next.ts @@ -0,0 +1,18 @@ +import path from 'path'; +import { AbstractVanillaExtractPlugin } from './plugin'; +import type { Compiler } from 'webpack'; + +const virtualNextFileLoader = require.resolve( + path.join( + path.dirname(require.resolve('../../package.json')), + 'virtualNextFileLoader', + ), +); + +export class VanillaExtractPlugin extends AbstractVanillaExtractPlugin { + static loader = virtualNextFileLoader; + + apply(compiler: Compiler) { + this.inject(compiler, 'virtualNextFileLoader'); + } +} diff --git a/packages/webpack-plugin/src/plugin.ts b/packages/webpack-plugin/src/plugin.ts new file mode 100644 index 000000000..4bc071cce --- /dev/null +++ b/packages/webpack-plugin/src/plugin.ts @@ -0,0 +1,110 @@ +import { cssFileFilter, IdentifierOption } from '@vanilla-extract/integration'; +import type { Compiler, RuleSetRule } from 'webpack'; + +import { ChildCompiler } from './compiler'; +import createCompat, { WebpackCompat } from './compat'; + +const pluginName = 'VanillaExtractPlugin'; + +function markCSSFilesAsSideEffects(compiler: Compiler, compat: WebpackCompat) { + compiler.hooks.normalModuleFactory.tap(pluginName, (nmf) => { + if (compat.isWebpack5) { + nmf.hooks.createModule.tap( + pluginName, + // @ts-expect-error CreateData is typed as 'object'... + (createData: { + matchResource?: string; + settings: { sideEffects?: boolean }; + }) => { + if ( + createData.matchResource && + (createData.matchResource.endsWith('.vanilla.css') || + createData.matchResource.endsWith('vanilla.virtual.css')) + ) { + createData.settings.sideEffects = true; + } + }, + ); + } else { + nmf.hooks.afterResolve.tap( + pluginName, + // @ts-expect-error Can't be typesafe for webpack 4 + (result: { + matchResource?: string; + settings: { sideEffects?: boolean }; + }) => { + if ( + result.matchResource && + (result.matchResource.endsWith('.vanilla.css') || + result.matchResource.endsWith('vanilla.virtual.css')) + ) { + result.settings.sideEffects = true; + } + }, + ); + } + }); +} + +export interface PluginOptions { + test?: RuleSetRule['test']; + identifiers?: IdentifierOption; + outputCss?: boolean; + externals?: any; + /** @deprecated */ + allowRuntime?: boolean; +} + +export abstract class AbstractVanillaExtractPlugin { + test: RuleSetRule['test']; + outputCss: boolean; + allowRuntime: boolean; + childCompiler: ChildCompiler; + identifiers?: IdentifierOption; + + constructor(options: PluginOptions = {}) { + const { + test = cssFileFilter, + outputCss = true, + externals, + allowRuntime, + identifiers, + } = options; + + if (allowRuntime !== undefined) { + console.warn('The "allowRuntime" option is deprecated.'); + } + + this.test = test; + this.outputCss = outputCss; + this.allowRuntime = allowRuntime ?? false; + this.childCompiler = new ChildCompiler(externals); + this.identifiers = identifiers; + } + + protected inject( + compiler: Compiler, + virtualLoader: 'virtualFileLoader' | 'virtualNextFileLoader', + ) { + const compat = createCompat( + Boolean(compiler.webpack && compiler.webpack.version), + ); + + markCSSFilesAsSideEffects(compiler, compat); + + compiler.options.module?.rules.splice(0, 0, { + test: this.test, + use: [ + { + loader: require.resolve('../loader'), + options: { + outputCss: this.outputCss, + childCompiler: this.childCompiler, + identifiers: this.identifiers, + virtualLoader: virtualLoader, + }, + }, + ], + }); + } +} diff --git a/packages/webpack-plugin/src/virtualNextFileLoader.ts b/packages/webpack-plugin/src/virtualNextFileLoader.ts new file mode 100644 index 000000000..f08caa18b --- /dev/null +++ b/packages/webpack-plugin/src/virtualNextFileLoader.ts @@ -0,0 +1,19 @@ +import { deserializeCss } from '@vanilla-extract/integration'; + +export default function (this: any) { + const callback = this.async(); + const resourceQuery = this.resourceQuery.slice(1); + + try { + const { source } = JSON.parse(decodeURIComponent(resourceQuery)); + deserializeCss(source) + .then((deserializedCss) => { + callback(null, deserializedCss); + }) + .catch((e) => { + callback(e as Error); + }); + } catch (e) { + callback(e as Error); + } +} diff --git a/packages/webpack-plugin/vanilla.virtual.css b/packages/webpack-plugin/vanilla.virtual.css new file mode 100644 index 000000000..24590f77c --- /dev/null +++ b/packages/webpack-plugin/vanilla.virtual.css @@ -0,0 +1,2 @@ +/* This is a noop file for extracted CSS source to point to */ +/* Webpack requires a file to exist on disk for virtual source files */ diff --git a/packages/webpack-plugin/virtualNextFileLoader/package.json b/packages/webpack-plugin/virtualNextFileLoader/package.json new file mode 100644 index 000000000..40483b98e --- /dev/null +++ b/packages/webpack-plugin/virtualNextFileLoader/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/vanilla-extract-webpack-plugin-virtualNextFileLoader.cjs.js", + "module": "dist/vanilla-extract-webpack-plugin-virtualNextFileLoader.esm.js" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aca68c25f..fd0a3a1e9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -289,12 +289,10 @@ importers: packages/next-plugin: specifiers: '@vanilla-extract/webpack-plugin': ^2.3.0 - browserslist: ^4.19.1 next: 12.3.4 webpack: ^5.36.1 dependencies: '@vanilla-extract/webpack-plugin': link:../webpack-plugin - browserslist: 4.21.4 devDependencies: next: 12.3.4 webpack: 5.64.2 @@ -5103,7 +5101,6 @@ packages: '@parcel/transformer-react-refresh-wrap': 2.8.3_@parcel+core@2.8.3 '@parcel/transformer-svg': 2.8.3_@parcel+core@2.8.3 transitivePeerDependencies: - - acorn - cssnano - postcss - purgecss @@ -5313,7 +5310,6 @@ packages: terser: 5.10.0 transitivePeerDependencies: - '@parcel/core' - - acorn dev: false /@parcel/package-manager/2.8.3: @@ -5791,7 +5787,6 @@ packages: terser: 5.10.0 v8-compile-cache: 2.3.0 transitivePeerDependencies: - - acorn - supports-color dev: false @@ -10598,6 +10593,7 @@ packages: /file-uri-to-path/1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + requiresBuild: true /filename-reserved-regex/2.0.0: resolution: {integrity: sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==} @@ -11547,8 +11543,6 @@ packages: param-case: 3.0.4 relateurl: 0.2.7 terser: 5.10.0 - transitivePeerDependencies: - - acorn dev: false /html-render-webpack-plugin/3.0.1_nzd6q2tbbwu7trnkwxj2tz54fy: @@ -11583,8 +11577,6 @@ packages: pretty-error: 4.0.0 tapable: 2.2.1 webpack: 5.64.2_webpack-cli@4.9.1 - transitivePeerDependencies: - - acorn dev: false /htmlnano/2.0.3_xx67fuclr4dzzmizs2x43q2w6i: @@ -18229,7 +18221,7 @@ packages: supports-hyperlinks: 2.2.0 dev: true - /terser-webpack-plugin/5.2.5_acorn@8.8.1+webpack@5.64.2: + /terser-webpack-plugin/5.2.5_dhjlflamz74snw5vqhz4pqvd2u: resolution: {integrity: sha512-3luOVHku5l0QBeYS8r4CdHYWEGMmIj3H1U64jgkdZzECcSOJAyJ9TjuqcQZvw1Y+4AOBN9SeYJPJmFn2cM4/2g==} engines: {node: '>= 10.13.0'} peerDependencies: @@ -18245,16 +18237,16 @@ packages: uglify-js: optional: true dependencies: + esbuild: 0.17.6 jest-worker: 27.3.1 schema-utils: 3.1.1 serialize-javascript: 6.0.0 source-map: 0.6.1 - terser: 5.10.0_acorn@8.8.1 - webpack: 5.64.2 - transitivePeerDependencies: - - acorn + terser: 5.10.0 + webpack: 5.64.2_esbuild@0.17.6 + dev: false - /terser-webpack-plugin/5.2.5_dmoaxqo2jb2cxajmot43jz3i7a: + /terser-webpack-plugin/5.2.5_webpack@5.64.2: resolution: {integrity: sha512-3luOVHku5l0QBeYS8r4CdHYWEGMmIj3H1U64jgkdZzECcSOJAyJ9TjuqcQZvw1Y+4AOBN9SeYJPJmFn2cM4/2g==} engines: {node: '>= 10.13.0'} peerDependencies: @@ -18270,23 +18262,17 @@ packages: uglify-js: optional: true dependencies: - esbuild: 0.17.6 jest-worker: 27.3.1 schema-utils: 3.1.1 serialize-javascript: 6.0.0 source-map: 0.6.1 - terser: 5.10.0_acorn@8.8.1 - webpack: 5.64.2_esbuild@0.17.6 - transitivePeerDependencies: - - acorn - dev: false + terser: 5.10.0 + webpack: 5.64.2 /terser/5.10.0: resolution: {integrity: sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==} engines: {node: '>=10'} hasBin: true - peerDependencies: - acorn: ^8.5.0 peerDependenciesMeta: acorn: optional: true @@ -18295,22 +18281,6 @@ packages: commander: 2.20.3 source-map: 0.7.3 source-map-support: 0.5.21 - dev: false - - /terser/5.10.0_acorn@8.8.1: - resolution: {integrity: sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==} - engines: {node: '>=10'} - hasBin: true - peerDependencies: - acorn: ^8.5.0 - peerDependenciesMeta: - acorn: - optional: true - dependencies: - acorn: 8.8.1 - commander: 2.20.3 - source-map: 0.7.3 - source-map-support: 0.5.21 /test-exclude/6.0.0: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} @@ -19683,7 +19653,7 @@ packages: neo-async: 2.6.2 schema-utils: 3.1.1 tapable: 2.2.1 - terser-webpack-plugin: 5.2.5_acorn@8.8.1+webpack@5.64.2 + terser-webpack-plugin: 5.2.5_webpack@5.64.2 watchpack: 2.3.0 webpack-sources: 3.2.2 transitivePeerDependencies: @@ -19722,7 +19692,7 @@ packages: neo-async: 2.6.2 schema-utils: 3.1.1 tapable: 2.2.1 - terser-webpack-plugin: 5.2.5_dmoaxqo2jb2cxajmot43jz3i7a + terser-webpack-plugin: 5.2.5_dhjlflamz74snw5vqhz4pqvd2u watchpack: 2.3.0 webpack-sources: 3.2.2 transitivePeerDependencies: @@ -19762,7 +19732,7 @@ packages: neo-async: 2.6.2 schema-utils: 3.1.1 tapable: 2.2.1 - terser-webpack-plugin: 5.2.5_acorn@8.8.1+webpack@5.64.2 + terser-webpack-plugin: 5.2.5_webpack@5.64.2 watchpack: 2.3.0 webpack-cli: 4.9.1_u5qztdvzsqoic44qnlo623kp4a webpack-sources: 3.2.2