Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions examples/pigment-css-nextjs-ts/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
Expand All @@ -21,9 +21,17 @@
],
"baseUrl": ".",
"paths": {
"react": ["./node_modules/@types/react"],
"react-dom": ["./node_modules/@types/react-dom"],
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
}
8 changes: 6 additions & 2 deletions packages/pigment-css-nextjs-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@pigment-css/nextjs-plugin",
"version": "0.0.31",
"author": "MUI Team",
"description": "Next.js integration for Pigment CSS.",
"description": "Next.js integration for Pigment CSS.",
"license": "MIT",
"main": "build/index.js",
"module": "build/index.mjs",
Expand All @@ -28,7 +28,11 @@
"typescript": "tsc --noEmit -p ."
},
"dependencies": {
"@pigment-css/unplugin": "workspace:^"
"@babel/preset-react": "^7.25.9",
"@babel/preset-typescript": "^7.26.0",
"@pigment-css/unplugin": "workspace:^",
"@wyw-in-js/shared": "^1.1.0",
"@wyw-in-js/transform": "^1.1.0"
},
"devDependencies": {
"next": "^15.1.3",
Expand Down
149 changes: 140 additions & 9 deletions packages/pigment-css-nextjs-plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import * as path from 'node:path';
import * as fs from 'node:fs';
import type { NextConfig } from 'next';
import { findPagesDir } from 'next/dist/lib/find-pages-dir';
import { webpack as webpackPlugin, extendTheme, type PigmentOptions } from '@pigment-css/unplugin';
import { slugify } from '@wyw-in-js/shared';
import { generateTokenCss } from '@pigment-css/react/utils';

export { type PigmentOptions };

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

function scanDirectory(dir: string, fileList: string[] = []): string[] {
const dirName = path.basename(dir);
if (
dirName === 'node_modules' ||
dirName === '.next' ||
dirName === '.git' ||
dir === path.resolve(__dirname, '..', 'virtual')
) {
return fileList;
}
try {
const files = fs.readdirSync(dir, { withFileTypes: true });
files.forEach((file) => {
const fullPath = path.join(dir, file.name);
if (file.isDirectory()) {
scanDirectory(fullPath, fileList);
} else if (file.isFile() && /\.(tsx|ts|jsx|js)$/.test(file.name)) {
fileList.push(fullPath);
}
});
} catch {
// Ignore
}
return fileList;
}

export function withPigment(nextConfig: NextConfig, pigmentConfig?: PigmentOptions) {
const { babelOptions = {}, asyncResolve, ...other } = pigmentConfig ?? {};
if (process.env.TURBOPACK === '1') {
// eslint-disable-next-line no-console
console.log(
`\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`,
);
return nextConfig;

// === Turbopack configuration (always prepared) ===
const virtualDir = path.resolve(__dirname, '../virtual');

if (!fs.existsSync(virtualDir)) {
fs.mkdirSync(virtualDir, { recursive: true });
}
// Pre-create virtual CSS files for all source files to bypass Turbopack's startup resolver cache
const sourceFiles = scanDirectory(process.cwd());
const activeSlugs = new Set<string>();

sourceFiles.forEach((file) => {
const slug = slugify(file);
const cssFileName = `${slug}.css`;
activeSlugs.add(cssFileName);
const cssPath = path.join(virtualDir, cssFileName);
if (!fs.existsSync(cssPath)) {
fs.writeFileSync(cssPath, '', 'utf8');
}
});
try {
const files = fs.readdirSync(virtualDir);
files.forEach((file) => {
if (file.endsWith('.css') && !activeSlugs.has(file)) {
fs.rmSync(path.join(virtualDir, file), { force: true });
}
});
} catch {
// Ignore
}
const cacheDir = path.resolve(process.cwd(), '.next/cache');
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir, { recursive: true });
}
const themeCachePath = path.join(cacheDir, 'pigment-theme.json');
const serializedTheme = JSON.stringify(other.theme ?? {}, (_key, value) => {
if (typeof value === 'function') {
return undefined;
}
return value;
});
fs.writeFileSync(themeCachePath, serializedTheme, 'utf8');

// Pre-generate token CSS while the full theme (with functions) is still available.
// JSON serialization strips generateStyleSheets, so generateTokenCss would return empty in the loader.
const tokenCssCachePath = path.join(cacheDir, 'pigment-token.css');
const tokenCss = generateTokenCss(other.theme);
fs.writeFileSync(tokenCssCachePath, tokenCss, 'utf8');

const turbopackLoaderItem = {
loader: require.resolve('./turbopack-loader'),
options: {
themeCachePath,
tokenCssCachePath,
transformLibraries: other.transformLibraries ?? [],
babelOptions: babelOptions ?? {},
css: other.css ?? null,
},
};

const turbopackNewRules = {
'*.ts': { loaders: [turbopackLoaderItem] },
'*.tsx': { loaders: [turbopackLoaderItem] },
'*.js': { loaders: [turbopackLoaderItem] },
'*.jsx': { loaders: [turbopackLoaderItem] },
'*.css': { loaders: [turbopackLoaderItem] },
};

type TurbopackRule = { loaders: Array<Record<string, unknown>> };
type TurbopackRules = Record<string, TurbopackRule>;

const mergeTurbopackRules = (
rulesToMerge: TurbopackRules,
existingRules: TurbopackRules = {},
): TurbopackRules => {
const mergedRules: TurbopackRules = { ...existingRules };
Object.entries(rulesToMerge).forEach(([key, rule]) => {
const existing = mergedRules[key];
if (existing) {
mergedRules[key] = {
...existing,
loaders: [...rule.loaders, ...existing.loaders],
};
} else {
mergedRules[key] = rule;
}
});
return mergedRules;
};

const nextConfigWithTurbo = nextConfig as NextConfig & {
turbopack?: {
rules?: TurbopackRules;
resolveAlias?: Record<string, string>;
};
};

// === Webpack configuration (lazy — only executed when Webpack is actually used) ===
const originalWebpack = nextConfig.webpack;

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

if (typeof nextConfig.webpack === 'function') {
return nextConfig.webpack(config, context);
if (typeof originalWebpack === 'function') {
return originalWebpack(config, context);
}
config.ignoreWarnings = config.ignoreWarnings ?? [];
config.ignoreWarnings.push({
module: /(zero-virtual\.css)|(react\/styles\.css)/,
});
return config;
};

// === Return both turbopack and webpack configs ===
return {
...nextConfig,
...nextConfigWithTurbo,
turbopack: {
...nextConfigWithTurbo.turbopack,
rules: mergeTurbopackRules(turbopackNewRules, nextConfigWithTurbo.turbopack?.rules),
resolveAlias: {
...nextConfigWithTurbo.turbopack?.resolveAlias,
'@pigment-css/nextjs-plugin/virtual': virtualDir,
},
},
webpack,
};
}
Expand Down
Loading
Loading