Skip to content

Commit 76705b1

Browse files
committed
[nextjs] Fix turbopack virtual CSS clearing
1 parent 67957b7 commit 76705b1

1 file changed

Lines changed: 140 additions & 9 deletions

File tree

  • packages/pigment-css-nextjs-plugin/src

packages/pigment-css-nextjs-plugin/src/index.ts

Lines changed: 140 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import * as path from 'node:path';
2+
import * as fs from 'node:fs';
23
import type { NextConfig } from 'next';
34
import { findPagesDir } from 'next/dist/lib/find-pages-dir';
45
import { webpack as webpackPlugin, extendTheme, type PigmentOptions } from '@pigment-css/unplugin';
6+
import { slugify } from '@wyw-in-js/shared';
7+
import { generateTokenCss } from '@pigment-css/react/utils';
58

69
export { type PigmentOptions };
710

@@ -10,15 +13,133 @@ const extractionFile = path.join(
1013
'zero-virtual.css',
1114
);
1215

16+
function scanDirectory(dir: string, fileList: string[] = []): string[] {
17+
const dirName = path.basename(dir);
18+
if (
19+
dirName === 'node_modules' ||
20+
dirName === '.next' ||
21+
dirName === '.git' ||
22+
dir === path.resolve(__dirname, '..', 'virtual')
23+
) {
24+
return fileList;
25+
}
26+
try {
27+
const files = fs.readdirSync(dir, { withFileTypes: true });
28+
files.forEach((file) => {
29+
const fullPath = path.join(dir, file.name);
30+
if (file.isDirectory()) {
31+
scanDirectory(fullPath, fileList);
32+
} else if (file.isFile() && /\.(tsx|ts|jsx|js)$/.test(file.name)) {
33+
fileList.push(fullPath);
34+
}
35+
});
36+
} catch {
37+
// Ignore
38+
}
39+
return fileList;
40+
}
41+
1342
export function withPigment(nextConfig: NextConfig, pigmentConfig?: PigmentOptions) {
1443
const { babelOptions = {}, asyncResolve, ...other } = pigmentConfig ?? {};
15-
if (process.env.TURBOPACK === '1') {
16-
// eslint-disable-next-line no-console
17-
console.log(
18-
`\x1B[33m${process.env.PACKAGE_NAME}: Turbo mode is not supported yet. Please disable it by removing the "--turbo" flag from your "next dev" command to use Pigment CSS.\x1B[39m`,
19-
);
20-
return nextConfig;
44+
45+
// === Turbopack configuration (always prepared) ===
46+
const virtualDir = path.resolve(__dirname, '../virtual');
47+
48+
if (!fs.existsSync(virtualDir)) {
49+
fs.mkdirSync(virtualDir, { recursive: true });
2150
}
51+
// Pre-create virtual CSS files for all source files to bypass Turbopack's startup resolver cache
52+
const sourceFiles = scanDirectory(process.cwd());
53+
const activeSlugs = new Set<string>();
54+
55+
sourceFiles.forEach((file) => {
56+
const slug = slugify(file);
57+
const cssFileName = `${slug}.css`;
58+
activeSlugs.add(cssFileName);
59+
const cssPath = path.join(virtualDir, cssFileName);
60+
if (!fs.existsSync(cssPath)) {
61+
fs.writeFileSync(cssPath, '', 'utf8');
62+
}
63+
});
64+
try {
65+
const files = fs.readdirSync(virtualDir);
66+
files.forEach((file) => {
67+
if (file.endsWith('.css') && !activeSlugs.has(file)) {
68+
fs.rmSync(path.join(virtualDir, file), { force: true });
69+
}
70+
});
71+
} catch {
72+
// Ignore
73+
}
74+
const cacheDir = path.resolve(process.cwd(), '.next/cache');
75+
if (!fs.existsSync(cacheDir)) {
76+
fs.mkdirSync(cacheDir, { recursive: true });
77+
}
78+
const themeCachePath = path.join(cacheDir, 'pigment-theme.json');
79+
const serializedTheme = JSON.stringify(other.theme ?? {}, (_key, value) => {
80+
if (typeof value === 'function') {
81+
return undefined;
82+
}
83+
return value;
84+
});
85+
fs.writeFileSync(themeCachePath, serializedTheme, 'utf8');
86+
87+
// Pre-generate token CSS while the full theme (with functions) is still available.
88+
// JSON serialization strips generateStyleSheets, so generateTokenCss would return empty in the loader.
89+
const tokenCssCachePath = path.join(cacheDir, 'pigment-token.css');
90+
const tokenCss = generateTokenCss(other.theme);
91+
fs.writeFileSync(tokenCssCachePath, tokenCss, 'utf8');
92+
93+
const turbopackLoaderItem = {
94+
loader: require.resolve('./turbopack-loader'),
95+
options: {
96+
themeCachePath,
97+
tokenCssCachePath,
98+
transformLibraries: other.transformLibraries ?? [],
99+
babelOptions: babelOptions ?? {},
100+
css: other.css ?? null,
101+
},
102+
};
103+
104+
const turbopackNewRules = {
105+
'*.ts': { loaders: [turbopackLoaderItem] },
106+
'*.tsx': { loaders: [turbopackLoaderItem] },
107+
'*.js': { loaders: [turbopackLoaderItem] },
108+
'*.jsx': { loaders: [turbopackLoaderItem] },
109+
'*.css': { loaders: [turbopackLoaderItem] },
110+
};
111+
112+
type TurbopackRule = { loaders: Array<Record<string, unknown>> };
113+
type TurbopackRules = Record<string, TurbopackRule>;
114+
115+
const mergeTurbopackRules = (
116+
rulesToMerge: TurbopackRules,
117+
existingRules: TurbopackRules = {},
118+
): TurbopackRules => {
119+
const mergedRules: TurbopackRules = { ...existingRules };
120+
Object.entries(rulesToMerge).forEach(([key, rule]) => {
121+
const existing = mergedRules[key];
122+
if (existing) {
123+
mergedRules[key] = {
124+
...existing,
125+
loaders: [...rule.loaders, ...existing.loaders],
126+
};
127+
} else {
128+
mergedRules[key] = rule;
129+
}
130+
});
131+
return mergedRules;
132+
};
133+
134+
const nextConfigWithTurbo = nextConfig as NextConfig & {
135+
turbopack?: {
136+
rules?: TurbopackRules;
137+
resolveAlias?: Record<string, string>;
138+
};
139+
};
140+
141+
// === Webpack configuration (lazy — only executed when Webpack is actually used) ===
142+
const originalWebpack = nextConfig.webpack;
22143

23144
const webpack: Exclude<NextConfig['webpack'], undefined> = (config, context) => {
24145
const { dir, dev, isServer, config: resolvedNextConfig } = context;
@@ -94,17 +215,27 @@ export function withPigment(nextConfig: NextConfig, pigmentConfig?: PigmentOptio
94215
}),
95216
);
96217

97-
if (typeof nextConfig.webpack === 'function') {
98-
return nextConfig.webpack(config, context);
218+
if (typeof originalWebpack === 'function') {
219+
return originalWebpack(config, context);
99220
}
100221
config.ignoreWarnings = config.ignoreWarnings ?? [];
101222
config.ignoreWarnings.push({
102223
module: /(zero-virtual\.css)|(react\/styles\.css)/,
103224
});
104225
return config;
105226
};
227+
228+
// === Return both turbopack and webpack configs ===
106229
return {
107-
...nextConfig,
230+
...nextConfigWithTurbo,
231+
turbopack: {
232+
...nextConfigWithTurbo.turbopack,
233+
rules: mergeTurbopackRules(turbopackNewRules, nextConfigWithTurbo.turbopack?.rules),
234+
resolveAlias: {
235+
...nextConfigWithTurbo.turbopack?.resolveAlias,
236+
'@pigment-css/nextjs-plugin/virtual': virtualDir,
237+
},
238+
},
108239
webpack,
109240
};
110241
}

0 commit comments

Comments
 (0)