diff --git a/.changeset/cool-rats-poke.md b/.changeset/cool-rats-poke.md new file mode 100644 index 000000000..8ccfd0160 --- /dev/null +++ b/.changeset/cool-rats-poke.md @@ -0,0 +1,9 @@ +--- +'@vanilla-extract/esbuild-plugin': minor +'@vanilla-extract/rollup-plugin': minor +'@vanilla-extract/sprinkles': minor +'@vanilla-extract/vite-plugin': minor +--- + +makes changes to the plugins integration (vite/webpack/esbuild/rollup) by allowing to pass those new callbacks to the `processVanillaFile` call +also exports the ProcessVanillaFileOptions & SprinklesProperties type diff --git a/.changeset/great-grapes-complain.md b/.changeset/great-grapes-complain.md new file mode 100644 index 000000000..f7d5396f2 --- /dev/null +++ b/.changeset/great-grapes-complain.md @@ -0,0 +1,6 @@ +--- +'@vanilla-extract/css': patch +--- + +fix(css): opti getDevPrefix by removing filePath regex +which was especially slow when used against filePath located somewhere in node_modules diff --git a/.changeset/moody-ears-study.md b/.changeset/moody-ears-study.md new file mode 100644 index 000000000..06ef9da29 --- /dev/null +++ b/.changeset/moody-ears-study.md @@ -0,0 +1,5 @@ +--- +'@vanilla-extract/vite-plugin': minor +--- + +allows customizing the forceEmitCssInSsrBuild option for the vite plugin (needed to make the HMR work with SSR frameworks for those not hard-coded, like vite-plugin-ssr) diff --git a/.changeset/perfect-rocks-check.md b/.changeset/perfect-rocks-check.md new file mode 100644 index 000000000..a4197a395 --- /dev/null +++ b/.changeset/perfect-rocks-check.md @@ -0,0 +1,7 @@ +--- +'@vanilla-extract/integration': minor +--- + +creates an AdapterContext (which is just a reference to the result of the adapter after the evalCode of processVanillaFile so anyone can retrieve the generated class names / CSS rules by file scopes + +adding a onEvaluated callback to processVanillaFile to retrieve the resulting mapping of classNames by property+value+condition using the AdapterContext diff --git a/.changeset/strong-windows-fail.md b/.changeset/strong-windows-fail.md new file mode 100644 index 000000000..186d061ef --- /dev/null +++ b/.changeset/strong-windows-fail.md @@ -0,0 +1,6 @@ +--- +'@vanilla-extract/integration': minor +--- + +adding a serializeVanillaModule callback like the current serializeVirtualCssPath callback, it allows customizing the behaviour +also exports the default implementation of serializeVanillaModule diff --git a/.changeset/tidy-gifts-tap.md b/.changeset/tidy-gifts-tap.md new file mode 100644 index 000000000..7a6f73792 --- /dev/null +++ b/.changeset/tidy-gifts-tap.md @@ -0,0 +1,5 @@ +--- +'@vanilla-extract/vite-plugin': minor +--- + +fix HMR diff --git a/.changeset/twelve-suns-dream.md b/.changeset/twelve-suns-dream.md new file mode 100644 index 000000000..259cd7628 --- /dev/null +++ b/.changeset/twelve-suns-dream.md @@ -0,0 +1,5 @@ +--- +'@vanilla-extract/vite-plugin': minor +--- + +using raw .css.ts from another package than config.root diff --git a/packages/css/src/identifier.ts b/packages/css/src/identifier.ts index 5dfe5b1ad..2bda08b05 100644 --- a/packages/css/src/identifier.ts +++ b/packages/css/src/identifier.ts @@ -15,12 +15,12 @@ function getDevPrefix({ if (debugFileName) { const { filePath } = getFileScope(); - const matches = filePath.match( - /(?[^\/\\]*)?[\/\\]?(?[^\/\\]*)\.css\.(ts|js|tsx|jsx|cjs|mjs)$/, - ); + const pathParts = filePath.split('/'); + const dir = pathParts[pathParts.length - 2]; + const fileNameParts = pathParts[pathParts.length - 1].split('.'); + const file = fileNameParts.length > 1 ? fileNameParts[0] : undefined; - if (matches && matches.groups) { - const { dir, file } = matches.groups; + if (dir || file) { parts.unshift(file && file !== 'index' ? file : dir); } } diff --git a/packages/esbuild-plugin/src/index.ts b/packages/esbuild-plugin/src/index.ts index c86aa777b..05275faba 100644 --- a/packages/esbuild-plugin/src/index.ts +++ b/packages/esbuild-plugin/src/index.ts @@ -9,12 +9,17 @@ import { vanillaExtractTransformPlugin, IdentifierOption, CompileOptions, + ProcessVanillaFileOptions, } from '@vanilla-extract/integration'; import type { Plugin } from 'esbuild'; const vanillaCssNamespace = 'vanilla-extract-css-ns'; -interface VanillaExtractPluginOptions { +interface VanillaExtractPluginOptions + extends Pick< + ProcessVanillaFileOptions, + 'onEvaluated' | 'serializeVanillaModule' + > { outputCss?: boolean; /** * @deprecated Use `esbuildOptions.external` instead. @@ -32,6 +37,8 @@ export function vanillaExtractPlugin({ processCss, identifiers, esbuildOptions, + onEvaluated, + serializeVanillaModule, }: VanillaExtractPluginOptions = {}): Plugin { if (runtime) { // If using runtime CSS then just apply fileScopes and debug IDs to code @@ -96,6 +103,8 @@ export function vanillaExtractPlugin({ filePath: path, outputCss, identOption, + onEvaluated, + serializeVanillaModule, }); return { diff --git a/packages/integration/src/index.ts b/packages/integration/src/index.ts index 9ed56d10d..05b510b2e 100644 --- a/packages/integration/src/index.ts +++ b/packages/integration/src/index.ts @@ -1,4 +1,5 @@ export { + defaultSerializeVanillaModule, processVanillaFile, parseFileScope, stringifyFileScope, @@ -15,4 +16,8 @@ export * from './filters'; export type { IdentifierOption } from './types'; export type { PackageInfo } from './packageInfo'; +export type { + AdapterContext, + ProcessVanillaFileOptions, +} from './processVanillaFile'; export type { CompileOptions } from './compile'; diff --git a/packages/integration/src/processVanillaFile.ts b/packages/integration/src/processVanillaFile.ts index 3090710de..c0dc10610 100644 --- a/packages/integration/src/processVanillaFile.ts +++ b/packages/integration/src/processVanillaFile.ts @@ -28,7 +28,7 @@ export function parseFileScope(serialisedFileScope: string): FileScope { }; } -interface ProcessVanillaFileOptions { +export interface ProcessVanillaFileOptions { source: string; filePath: string; outputCss?: boolean; @@ -38,41 +38,66 @@ interface ProcessVanillaFileOptions { fileScope: FileScope; source: string; }) => string | Promise; + onEvaluated?: (args: { + source: string; + context: AdapterContext; + evalResult: Record; + filePath: string; + }) => void; + serializeVanillaModule?: ( + cssImports: Array, + exports: Record, + context: AdapterContext, + filePath: string, + ) => string; +} + +export interface AdapterContext { + cssByFileScope: Map; + localClassNames: Set; + composedClassLists: Composition[]; + usedCompositions: Set; } + +type Css = Parameters[0]; +type Composition = Parameters[0]; + export async function processVanillaFile({ source, filePath, outputCss = true, identOption = process.env.NODE_ENV === 'production' ? 'short' : 'debug', serializeVirtualCssPath, + serializeVanillaModule, + onEvaluated, }: ProcessVanillaFileOptions) { - type Css = Parameters[0]; - type Composition = Parameters[0]; - - const cssByFileScope = new Map>(); - const localClassNames = new Set(); - const composedClassLists: Array = []; - const usedCompositions = new Set(); + const context: AdapterContext = { + cssByFileScope: new Map>(), + localClassNames: new Set(), + composedClassLists: [], + usedCompositions: new Set(), + }; const cssAdapter: Adapter = { appendCss: (css, fileScope) => { if (outputCss) { const serialisedFileScope = stringifyFileScope(fileScope); - const fileScopeCss = cssByFileScope.get(serialisedFileScope) ?? []; + const fileScopeCss = + context.cssByFileScope.get(serialisedFileScope) ?? []; fileScopeCss.push(css); - cssByFileScope.set(serialisedFileScope, fileScopeCss); + context.cssByFileScope.set(serialisedFileScope, fileScopeCss); } }, registerClassName: (className) => { - localClassNames.add(className); + context.localClassNames.add(className); }, registerComposition: (composedClassList) => { - composedClassLists.push(composedClassList); + context.composedClassLists.push(composedClassList); }, markCompositionUsed: (identifier) => { - usedCompositions.add(identifier); + context.usedCompositions.add(identifier); }, onEndFileScope: () => {}, getIdentOption: () => identOption, @@ -96,16 +121,17 @@ export async function processVanillaFile({ { console, process, __adapter__: cssAdapter }, true, ); + onEvaluated?.({ source, context, evalResult, filePath }); process.env.NODE_ENV = currentNodeEnv; const cssImports = []; - for (const [serialisedFileScope, fileScopeCss] of cssByFileScope) { + for (const [serialisedFileScope, fileScopeCss] of context.cssByFileScope) { const fileScope = parseFileScope(serialisedFileScope); const css = transformCss({ - localClassNames: Array.from(localClassNames), - composedClassLists, + localClassNames: Array.from(context.localClassNames), + composedClassLists: context.composedClassLists, cssObjs: fileScopeCss, }).join('\n'); @@ -148,16 +174,12 @@ export async function processVanillaFile({ true, ); - const unusedCompositions = composedClassLists - .filter(({ identifier }) => !usedCompositions.has(identifier)) - .map(({ identifier }) => identifier); - - const unusedCompositionRegex = - unusedCompositions.length > 0 - ? RegExp(`(${unusedCompositions.join('|')})\\s`, 'g') - : null; - - return serializeVanillaModule(cssImports, evalResult, unusedCompositionRegex); + return (serializeVanillaModule ?? defaultSerializeVanillaModule)( + cssImports, + evalResult, + context, + filePath, + ); } export function stringifyExports( @@ -246,11 +268,20 @@ export function stringifyExports( ); } -function serializeVanillaModule( +export function defaultSerializeVanillaModule( cssImports: Array, exports: Record, - unusedCompositionRegex: RegExp | null, + context: AdapterContext, ) { + const unusedCompositions = context.composedClassLists + .filter(({ identifier }) => !context.usedCompositions.has(identifier)) + .map(({ identifier }) => identifier); + + const unusedCompositionRegex = + unusedCompositions.length > 0 + ? RegExp(`(${unusedCompositions.join('|')})\\s`, 'g') + : null; + const recipeImports = new Set(); const moduleExports = Object.keys(exports).map((key) => diff --git a/packages/rollup-plugin/src/index.ts b/packages/rollup-plugin/src/index.ts index 95c7ab4af..ec139c3b1 100644 --- a/packages/rollup-plugin/src/index.ts +++ b/packages/rollup-plugin/src/index.ts @@ -7,12 +7,17 @@ import { getSourceFromVirtualCssFile, virtualCssFileFilter, CompileOptions, + ProcessVanillaFileOptions, } from '@vanilla-extract/integration'; import { posix } from 'path'; const { relative, normalize, dirname } = posix; -interface Options { +interface Options + extends Pick< + ProcessVanillaFileOptions, + 'onEvaluated' | 'serializeVanillaModule' + > { identifiers?: IdentifierOption; cwd?: string; esbuildOptions?: CompileOptions['esbuildOptions']; @@ -21,6 +26,8 @@ export function vanillaExtractPlugin({ identifiers, cwd = process.cwd(), esbuildOptions, + onEvaluated, + serializeVanillaModule, }: Options = {}): Plugin { const emittedFiles = new Map(); const isProduction = process.env.NODE_ENV === 'production'; @@ -55,6 +62,8 @@ export function vanillaExtractPlugin({ source, filePath, identOption, + onEvaluated, + serializeVanillaModule, }); return { code: output, diff --git a/packages/sprinkles/src/index.ts b/packages/sprinkles/src/index.ts index b082881d3..98581a60e 100644 --- a/packages/sprinkles/src/index.ts +++ b/packages/sprinkles/src/index.ts @@ -12,6 +12,7 @@ import { createSprinkles as internalCreateSprinkles, } from './createSprinkles'; import { SprinklesProperties, ResponsiveArrayConfig } from './types'; +export type { SprinklesProperties } from './types'; export { createNormalizeValueFn, createMapValueFn } from './createUtils'; export type { ConditionalValue, RequiredConditionalValue } from './createUtils'; diff --git a/packages/vite-plugin/src/index.ts b/packages/vite-plugin/src/index.ts index 21195e75d..06271e0ae 100644 --- a/packages/vite-plugin/src/index.ts +++ b/packages/vite-plugin/src/index.ts @@ -10,6 +10,7 @@ import { IdentifierOption, getPackageInfo, CompileOptions, + ProcessVanillaFileOptions, transform, } from '@vanilla-extract/integration'; import { PostCSSConfigResult, resolvePostcssConfig } from './postcss'; @@ -19,22 +20,32 @@ const styleUpdateEvent = (fileId: string) => const virtualExtCss = '.vanilla.css'; const virtualExtJs = '.vanilla.js'; +const virtualRE = /.vanilla.(css|js)$/; -interface Options { +export interface VanillaExtractPluginOptions + extends Pick< + ProcessVanillaFileOptions, + 'onEvaluated' | 'serializeVanillaModule' + > { identifiers?: IdentifierOption; esbuildOptions?: CompileOptions['esbuildOptions']; + forceEmitCssInSsrBuild?: boolean; } export function vanillaExtractPlugin({ identifiers, esbuildOptions, -}: Options = {}): Plugin { + onEvaluated, + serializeVanillaModule, + forceEmitCssInSsrBuild: _forceEmitCssInSsrBuild, +}: VanillaExtractPluginOptions = {}): Plugin { let config: ResolvedConfig; let server: ViteDevServer; let postCssConfig: PostCSSConfigResult | null; const cssMap = new Map(); - let forceEmitCssInSsrBuild: boolean = !!process.env.VITE_RSC_BUILD; - let packageName: string; + let forceEmitCssInSsrBuild: boolean = + _forceEmitCssInSsrBuild || !!process.env.VITE_RSC_BUILD; + let packageInfos: ReturnType; const getAbsoluteVirtualFileId = (source: string) => normalizePath(path.join(config.root, source)); @@ -62,7 +73,7 @@ export function vanillaExtractPlugin({ }, async configResolved(resolvedConfig) { config = resolvedConfig; - packageName = getPackageInfo(config.root).name; + packageInfos = getPackageInfo(config.root); if (config.command === 'serve') { postCssConfig = await resolvePostcssConfig(config); @@ -78,12 +89,39 @@ export function vanillaExtractPlugin({ forceEmitCssInSsrBuild = true; } }, - resolveId(source) { + // Re-parse .css.ts files when they change + async handleHotUpdate({ file, modules }) { + if (!cssFileFilter.test(file)) return; + try { + const virtuals: any[] = []; + const invalidate = (type: string) => { + const found = server.moduleGraph.getModulesByFile(`${file}${type}`); + found?.forEach((m) => { + virtuals.push(m); + return server.moduleGraph.invalidateModule(m); + }); + }; + invalidate(virtualExtCss); + invalidate(virtualExtJs); + // load new CSS + await server.ssrLoadModule(file); + return [...modules, ...virtuals]; + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + throw e; + } + }, + // Convert .vanilla.(js|css) URLs to their absolute version + resolveId(source, importer) { const [validId, query] = source.split('?'); if (!validId.endsWith(virtualExtCss) && !validId.endsWith(virtualExtJs)) { return; } + // while source is .css.ts.vanilla.(js|css), the importer should always be a .css.ts or .html file + if (!importer) return; + // Absolute paths seem to occur often in monorepos, where files are // imported from outside the config root. const absoluteId = source.startsWith(config.root) @@ -93,18 +131,25 @@ export function vanillaExtractPlugin({ // There should always be an entry in the `cssMap` here. // The only valid scenario for a missing one is if someone had written // a file in their app using the .vanilla.js/.vanilla.css extension - if (cssMap.has(absoluteId)) { - // Keep the original query string for HMR. - return absoluteId + (query ? `?${query}` : ''); - } + // Keep the original query string for HMR. + return absoluteId + (query ? `?${query}` : ''); }, - load(id) { + // Provide virtual CSS content + async load(id) { const [validId] = id.split('?'); - if (!cssMap.has(validId)) { + if (!virtualRE.test(validId)) { return; } + if (!cssMap.has(validId)) { + // Try to parse the parent + const parentId = validId.replace(virtualRE, ''); + await server.ssrLoadModule(parentId); + // Now we should have the CSS + if (!cssMap.has(validId)) return; + } + const css = cssMap.get(validId); if (typeof css !== 'string') { @@ -150,19 +195,25 @@ export function vanillaExtractPlugin({ ssr = ssrParam?.ssr; } + let filePackageInfos = packageInfos; + const fileDirectory = path.dirname(validId); + if (!isSubDir(packageInfos.dirname, fileDirectory)) { + filePackageInfos = getPackageInfo(fileDirectory); + } + if (ssr && !forceEmitCssInSsrBuild) { return transform({ source: code, filePath: normalizePath(validId), - rootPath: config.root, - packageName, + rootPath: filePackageInfos.dirname, + packageName: filePackageInfos.name, identOption, }); } const { source, watchFiles } = await compile({ filePath: validId, - cwd: config.root, + cwd: filePackageInfos.dirname, esbuildOptions, identOption, }); @@ -179,9 +230,11 @@ export function vanillaExtractPlugin({ source, filePath: validId, identOption, + onEvaluated, + serializeVanillaModule, serializeVirtualCssPath: async ({ fileScope, source }) => { const rootRelativeId = `${fileScope.filePath}${ - config.command === 'build' || (ssr && forceEmitCssInSsrBuild) + config.command === 'build' || forceEmitCssInSsrBuild ? virtualExtCss : virtualExtJs }`; @@ -204,7 +257,7 @@ export function vanillaExtractPlugin({ if ( server && cssMap.has(absoluteId) && - cssMap.get(absoluteId) !== source + cssMap.get(absoluteId) !== cssSource ) { const { moduleGraph } = server; const [module] = Array.from( @@ -241,3 +294,10 @@ export function vanillaExtractPlugin({ }, }; } + +function isSubDir(parent: string, dir: string) { + const relative = path.relative(parent, dir); + return Boolean( + relative && !relative.startsWith('..') && !path.isAbsolute(relative), + ); +} diff --git a/packages/webpack-plugin/src/index.ts b/packages/webpack-plugin/src/index.ts index beebad067..b9351d63a 100644 --- a/packages/webpack-plugin/src/index.ts +++ b/packages/webpack-plugin/src/index.ts @@ -1,4 +1,8 @@ -import { cssFileFilter, IdentifierOption } from '@vanilla-extract/integration'; +import { + cssFileFilter, + IdentifierOption, + ProcessVanillaFileOptions, +} from '@vanilla-extract/integration'; import type { Compiler, RuleSetRule } from 'webpack'; import { ChildCompiler } from './compiler'; @@ -58,6 +62,8 @@ export class VanillaExtractPlugin { allowRuntime: boolean; childCompiler: ChildCompiler; identifiers?: IdentifierOption; + onEvaluated?: ProcessVanillaFileOptions['onEvaluated']; + serializeVanillaModule?: ProcessVanillaFileOptions['onEvaluated']; constructor(options: PluginOptions = {}) { const { @@ -95,6 +101,8 @@ export class VanillaExtractPlugin { outputCss: this.outputCss, childCompiler: this.childCompiler, identifiers: this.identifiers, + onEvaluated: this.onEvaluated, + serializeVanillaModule: this.serializeVanillaModule, }, }, ], diff --git a/packages/webpack-plugin/src/loader.ts b/packages/webpack-plugin/src/loader.ts index 0812b6838..a9cf1b003 100644 --- a/packages/webpack-plugin/src/loader.ts +++ b/packages/webpack-plugin/src/loader.ts @@ -7,6 +7,7 @@ import { transform, serializeCss, getPackageInfo, + ProcessVanillaFileOptions, } from '@vanilla-extract/integration'; import type { LoaderContext } from './types'; @@ -25,7 +26,11 @@ const emptyCssExtractionFile = path.join( 'extracted.js', ); -interface LoaderOptions { +interface LoaderOptions + extends Pick< + ProcessVanillaFileOptions, + 'onEvaluated' | 'serializeVanillaModule' + > { outputCss: boolean; identifiers?: IdentifierOption; } @@ -63,9 +68,13 @@ 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, + onEvaluated, + serializeVanillaModule, + } = loaderUtils.getOptions(this) as InternalLoaderOptions; const log = debug( `vanilla-extract:loader:${formatResourcePath(this.resourcePath)}`, @@ -95,6 +104,8 @@ export function pitch(this: LoaderContext) { outputCss, filePath: this.resourcePath, identOption: defaultIdentifierOption(this.mode, identifiers), + onEvaluated, + serializeVanillaModule, serializeVirtualCssPath: async ({ fileName, source }) => { const serializedCss = await serializeCss(source); const virtualResourceLoader = `${virtualLoader}?${JSON.stringify({