@tailwind-merge/next 0.0.0-dev.50b1d1e9f69ac68604be38dabea94cccdc6b070a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +41 -0
- package/dist/index.d.mts +21 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +124 -0
- package/dist/index.mjs.map +1 -0
- package/dist/loader.d.mts +23 -0
- package/dist/loader.d.mts.map +1 -0
- package/dist/loader.mjs +2815 -0
- package/dist/loader.mjs.map +1 -0
- package/dist/options-B1iOW6tJ.d.mts +38 -0
- package/dist/options-B1iOW6tJ.d.mts.map +1 -0
- package/dist/runtime.d.mts +2 -0
- package/dist/runtime.mjs +7 -0
- package/dist/runtime.mjs.map +1 -0
- package/package.json +80 -0
- package/src/index.ts +162 -0
- package/src/loader.ts +183 -0
- package/src/options.ts +81 -0
- package/src/runtime.ts +21 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Dany Castillo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# @tailwind-merge/next
|
|
2
|
+
|
|
3
|
+
A [Next.js](https://nextjs.org) plugin that configures [tailwind-merge](https://github.com/dcastil/tailwind-merge) for your project's own [Tailwind CSS](https://tailwindcss.com) theme — automatically, at build time, with nothing to maintain by hand. Works with Turbopack and webpack.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
// next.config.ts
|
|
7
|
+
import { withTailwindMerge } from '@tailwind-merge/next'
|
|
8
|
+
|
|
9
|
+
export default withTailwindMerge({
|
|
10
|
+
// your Next.js config
|
|
11
|
+
})
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { twMerge } from '@tailwind-merge/next/runtime'
|
|
16
|
+
|
|
17
|
+
// With `--text-huge: 2.5rem` in your @theme:
|
|
18
|
+
twMerge('text-huge text-sm')
|
|
19
|
+
// → 'text-sm' — plain tailwind-merge would keep both, misreading text-huge as a color
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- Zero configuration: your Tailwind CSS entrypoint is auto-detected, the merge config is generated from the theme Tailwind actually resolves
|
|
23
|
+
- One package to add: tailwind-merge is supplied as the plugin's runtime dependency
|
|
24
|
+
- Nothing written to disk: the generated module replaces the runtime import inside the bundler pipeline, so no checked-in artifacts and no TypeScript-server churn
|
|
25
|
+
- Consistent everywhere: server components, server rendering, and the browser all get the same generated module
|
|
26
|
+
- Quiet in development: the config regenerates only when your Tailwind configuration changes
|
|
27
|
+
- Small in production: the config is pruned to the classes found in your sources — the same files Tailwind scans — with substantial savings in the [measured project samples](https://github.com/dcastil/tailwind-merge/blob/50b1d1e9f69ac68604be38dabea94cccdc6b070a/agents/configurator-performance.md)
|
|
28
|
+
- Declares support for Next.js 16 with Turbopack or webpack and the Tailwind CSS v4.3 line; see [coverage limits](https://github.com/dcastil/tailwind-merge/blob/50b1d1e9f69ac68604be38dabea94cccdc6b070a/packages/next/docs/limitations.md)
|
|
29
|
+
|
|
30
|
+
> **Status: pre-release, pre-1.0.** There is no stable release yet. Dev builds of every commit on `main` are published to npm under the `dev` tag for evaluation — see [Getting started](https://github.com/dcastil/tailwind-merge/blob/50b1d1e9f69ac68604be38dabea94cccdc6b070a/packages/next/docs/getting-started.md#availability-and-installation). Treat the plugin as not production-ready until 1.0.0 — see [Versioning](https://github.com/dcastil/tailwind-merge/blob/50b1d1e9f69ac68604be38dabea94cccdc6b070a/packages/next/docs/versioning.md).
|
|
31
|
+
|
|
32
|
+
## Get started
|
|
33
|
+
|
|
34
|
+
- [What is it for](https://github.com/dcastil/tailwind-merge/blob/50b1d1e9f69ac68604be38dabea94cccdc6b070a/packages/next/docs/what-is-it-for.md)
|
|
35
|
+
- [Getting started](https://github.com/dcastil/tailwind-merge/blob/50b1d1e9f69ac68604be38dabea94cccdc6b070a/packages/next/docs/getting-started.md)
|
|
36
|
+
- [How it works](https://github.com/dcastil/tailwind-merge/blob/50b1d1e9f69ac68604be38dabea94cccdc6b070a/packages/next/docs/how-it-works.md)
|
|
37
|
+
- [API reference](https://github.com/dcastil/tailwind-merge/blob/50b1d1e9f69ac68604be38dabea94cccdc6b070a/packages/next/docs/api-reference.md)
|
|
38
|
+
- [Limitations](https://github.com/dcastil/tailwind-merge/blob/50b1d1e9f69ac68604be38dabea94cccdc6b070a/packages/next/docs/limitations.md)
|
|
39
|
+
- [Versioning](https://github.com/dcastil/tailwind-merge/blob/50b1d1e9f69ac68604be38dabea94cccdc6b070a/packages/next/docs/versioning.md)
|
|
40
|
+
|
|
41
|
+
Using Vite instead? [@tailwind-merge/vite](https://github.com/dcastil/tailwind-merge/blob/50b1d1e9f69ac68604be38dabea94cccdc6b070a/packages/vite/README.md) is the same idea for Vite apps. For work on the plugin itself, read the [Next.js development guide](https://github.com/dcastil/tailwind-merge/blob/50b1d1e9f69ac68604be38dabea94cccdc6b070a/agents/next-plugin.md).
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { n as PruneOptions, r as TailwindMergeOptions } from "./options-B1iOW6tJ.mjs";
|
|
2
|
+
import { NextConfig } from "next";
|
|
3
|
+
//#region src/index.d.ts
|
|
4
|
+
/** The function form of `next.config`, which Next.js calls with the current phase (see `next/constants`) and its default config. */
|
|
5
|
+
type NextConfigFunction = (phase: string, context: {
|
|
6
|
+
defaultConfig: NextConfig;
|
|
7
|
+
}) => NextConfig | Promise<NextConfig>;
|
|
8
|
+
/**
|
|
9
|
+
* Configures tailwind-merge for the project's own Tailwind CSS in a Next.js app.
|
|
10
|
+
*
|
|
11
|
+
* Wrap the Next.js config with it and import from the runtime subpath: `import { twMerge } from '@tailwind-merge/next/runtime'`. While Next.js compiles the app, that import resolves to the module generated from the project's Tailwind theme by the inlined plugin core and configurator; outside Next.js it resolves to the real runtime.ts and serves default tailwind-merge behavior. Repository integration goals and invariants live in agents/next-plugin.md.
|
|
12
|
+
*
|
|
13
|
+
* Mechanics: neither Turbopack nor webpack offers virtual modules through the Next.js config, but both run webpack-style loaders, so the plugin registers a loader rule for this package's own `runtime.mjs` with both bundlers and lets the loader (src/loader.ts) replace the file's content with the generated module. The rules are keyed on the file name plus the legal comment inside the file, not on a path in node_modules, because Turbopack matches real paths — a symlinked install (`pnpm link`, a workspace) would otherwise fall through to the default config unnoticed. Dev and build get separate Turbopack rules through the built-in `development`/`production` conditions; the webpack hook reads the mode from its context. `transpilePackages` keeps the Pages Router's server bundle from externalizing the package: an externalized runtime would `require()` the on-disk fallback at request time while the client bundle carries the generated module, and the two would merge classes differently.
|
|
14
|
+
*
|
|
15
|
+
* The dev loop is deliberately quiet: the loader registers the files of the CSS configuration graph as dependencies (never the app's sources, unless `prune.dev` asks for it), so Next.js re-runs it only when one of those changes, and a regeneration that produces identical code leaves the bundlers' module hashes unchanged. Production builds additionally prune the config to the classes found in the project's sources (`prune` option).
|
|
16
|
+
*/
|
|
17
|
+
declare function withTailwindMerge(nextConfig: NextConfigFunction, options?: TailwindMergeOptions): NextConfigFunction;
|
|
18
|
+
declare function withTailwindMerge(nextConfig?: NextConfig, options?: TailwindMergeOptions): NextConfig;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { NextConfigFunction, type PruneOptions, type TailwindMergeOptions, withTailwindMerge };
|
|
21
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;;KAeY,sBACR,eACA;EAAW,eAAe;MACzB,aAAa,QAAQ;;;;;;;;;;iBAWV,kBACZ,YAAY,oBACZ,UAAU,uBACX;iBACa,kBACZ,aAAa,YACb,UAAU,uBACX"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
//#region src/options.ts
|
|
5
|
+
/** Resolves the `prune` option's shorthand forms and defaults into the full object. */
|
|
6
|
+
function resolvePruneOptions(option) {
|
|
7
|
+
if (option === false) return {
|
|
8
|
+
build: false,
|
|
9
|
+
dev: false,
|
|
10
|
+
log: false
|
|
11
|
+
};
|
|
12
|
+
if (option === true || option === void 0) return {
|
|
13
|
+
build: true,
|
|
14
|
+
dev: false,
|
|
15
|
+
log: true
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
build: option.build ?? true,
|
|
19
|
+
dev: option.dev ?? false,
|
|
20
|
+
log: option.log ?? true
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** The loader options for one mode, with only the keys the user set — the loader applies the defaults of the underlying generator for the rest. */
|
|
24
|
+
function resolveLoaderOptions(options, mode) {
|
|
25
|
+
const prune = resolvePruneOptions(options.prune);
|
|
26
|
+
const resolved = {
|
|
27
|
+
mode,
|
|
28
|
+
prune: mode === "build" ? prune.build : prune.dev,
|
|
29
|
+
log: prune.log
|
|
30
|
+
};
|
|
31
|
+
if (options.css !== void 0) resolved.css = options.css;
|
|
32
|
+
if (options.cacheSize !== void 0) resolved.cacheSize = options.cacheSize;
|
|
33
|
+
if (options.encoding !== void 0) resolved.encoding = options.encoding;
|
|
34
|
+
return resolved;
|
|
35
|
+
}
|
|
36
|
+
/** The same options as a value the bundler configuration types accept: Next's loader option type has no room for `undefined`, which TypeScript reads into every optional key, so the object is rebuilt from its present keys. */
|
|
37
|
+
function serializableLoaderOptions(options) {
|
|
38
|
+
return Object.fromEntries(Object.entries(options).filter((entry) => entry[1] !== void 0));
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/index.ts
|
|
42
|
+
function withTailwindMerge(nextConfig = {}, options = {}) {
|
|
43
|
+
if (typeof nextConfig === "function") return async (phase, context) => applyToConfig(await nextConfig(phase, context), options);
|
|
44
|
+
return applyToConfig(nextConfig, options);
|
|
45
|
+
}
|
|
46
|
+
function applyToConfig(config, options) {
|
|
47
|
+
warnWithoutCompilerConfig();
|
|
48
|
+
const rules = config.turbopack?.rules ?? {};
|
|
49
|
+
const existing = rules[RULE_GLOB];
|
|
50
|
+
const existingItems = existing === void 0 ? [] : Array.isArray(existing) ? existing : [existing];
|
|
51
|
+
const transpilePackages = config.transpilePackages ?? [];
|
|
52
|
+
return {
|
|
53
|
+
...config,
|
|
54
|
+
transpilePackages: transpilePackages.includes(PACKAGE_NAME) ? transpilePackages : [...transpilePackages, PACKAGE_NAME],
|
|
55
|
+
turbopack: {
|
|
56
|
+
...config.turbopack,
|
|
57
|
+
rules: {
|
|
58
|
+
...rules,
|
|
59
|
+
[RULE_GLOB]: [...existingItems, ...turbopackRules(options)]
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
webpack(webpackConfig, context) {
|
|
63
|
+
webpackConfig.module.rules.push({
|
|
64
|
+
test: isRuntimeModule,
|
|
65
|
+
use: [{
|
|
66
|
+
loader: LOADER_PATH,
|
|
67
|
+
options: serializableLoaderOptions(resolveLoaderOptions(options, context.dev ? "dev" : "build"))
|
|
68
|
+
}]
|
|
69
|
+
});
|
|
70
|
+
return config.webpack ? config.webpack(webpackConfig, context) : webpackConfig;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
let warnedWithoutCompilerConfig = false;
|
|
75
|
+
/**
|
|
76
|
+
* Turbopack applies loader rules to code in node_modules only when the project has a `tsconfig.json` or `jsconfig.json` — even an empty one; the content does not matter (observed with Next.js 16.3, reproduced with a bare `*.mjs` rule). Without one, the runtime subpath would silently keep tailwind-merge's default behavior under Turbopack, so say so once per process. create-next-app writes one of the two files for every project, which keeps this rare. The working directory stands in for the project directory, which the config API does not expose.
|
|
77
|
+
*/
|
|
78
|
+
function warnWithoutCompilerConfig() {
|
|
79
|
+
if (warnedWithoutCompilerConfig) return;
|
|
80
|
+
warnedWithoutCompilerConfig = true;
|
|
81
|
+
const directory = process.cwd();
|
|
82
|
+
if (!["tsconfig.json", "jsconfig.json"].some((file) => existsSync(path.join(directory, file)))) console.warn(`[${PACKAGE_NAME}] No tsconfig.json or jsconfig.json found in ${directory}. Turbopack only applies the plugin's loader to installed packages when the project has one, so without it the runtime import keeps tailwind-merge's default configuration. Add an empty jsconfig.json if the project has no TypeScript configuration.`);
|
|
83
|
+
}
|
|
84
|
+
/** One rule per mode: Turbopack evaluates the plugin's config once for `next dev` and `next build` alike, so the mode has to be a rule condition rather than a value computed at config time. */
|
|
85
|
+
function turbopackRules(options) {
|
|
86
|
+
return ["dev", "build"].map((mode) => ({
|
|
87
|
+
condition: { all: [
|
|
88
|
+
{ path: RUNTIME_PATH_PATTERN },
|
|
89
|
+
{ content: RUNTIME_MARKER_PATTERN },
|
|
90
|
+
mode === "dev" ? "development" : "production"
|
|
91
|
+
] },
|
|
92
|
+
loaders: [{
|
|
93
|
+
loader: LOADER_PATH,
|
|
94
|
+
options: serializableLoaderOptions(resolveLoaderOptions(options, mode))
|
|
95
|
+
}]
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
/** webpack hands rules the resolved resource path, with symlinks resolved by default: compare against both spellings of this package's runtime file. */
|
|
99
|
+
function isRuntimeModule(file) {
|
|
100
|
+
return file === RUNTIME_PATH || file === runtimeRealPath();
|
|
101
|
+
}
|
|
102
|
+
let resolvedRuntimeRealPath;
|
|
103
|
+
function runtimeRealPath() {
|
|
104
|
+
if (resolvedRuntimeRealPath === void 0) try {
|
|
105
|
+
resolvedRuntimeRealPath = realpathSync(RUNTIME_PATH);
|
|
106
|
+
} catch {
|
|
107
|
+
resolvedRuntimeRealPath = RUNTIME_PATH;
|
|
108
|
+
}
|
|
109
|
+
return resolvedRuntimeRealPath;
|
|
110
|
+
}
|
|
111
|
+
const PACKAGE_NAME = "@tailwind-merge/next";
|
|
112
|
+
/** Turbopack rule keys are file-name globs; the conditions below narrow the rule to this package's runtime file. */
|
|
113
|
+
const RULE_GLOB = "*.mjs";
|
|
114
|
+
/** Turbopack matches project-relative real paths with forward slashes; only the file name is fixed across install layouts. */
|
|
115
|
+
const RUNTIME_PATH_PATTERN = /(^|\/)runtime\.mjs$/;
|
|
116
|
+
/** The legal comment at the top of src/runtime.ts, which the build preserves — the one part of the runtime file's content the rule can rely on across install layouts. */
|
|
117
|
+
const RUNTIME_MARKER_PATTERN = /@tailwind-merge\/next runtime fallback/;
|
|
118
|
+
/** Both bundlers accept a loader as an absolute file path, which sidesteps loader-name resolution and the exports map. Built and source layouts agree on the relative location. */
|
|
119
|
+
const LOADER_PATH = fileURLToPath(new URL("./loader.mjs", import.meta.url));
|
|
120
|
+
const RUNTIME_PATH = fileURLToPath(new URL("./runtime.mjs", import.meta.url));
|
|
121
|
+
//#endregion
|
|
122
|
+
export { withTailwindMerge };
|
|
123
|
+
|
|
124
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/options.ts","../src/index.ts"],"sourcesContent":["export interface TailwindMergeOptions {\n /** Path to the project's Tailwind CSS entrypoint, relative to the project directory. When omitted, the entrypoint is auto-detected within the project. Set this to disambiguate themes or select an entrypoint outside the discovery scan. */\n css?: string\n /** LRU cache size of the generated `twMerge`, passed through to the generated config. Defaults to tailwind-merge's default. */\n cacheSize?: number\n /** How theme scales are encoded in the generated config: `'compact'` (default) picks the smallest matcher even when it accepts names beyond the theme, `'exact'` enumerates finite names to avoid that overmatching, at a size cost; arbitrary-value types remain approximate. See the configurator's docs for the tradeoff. */\n encoding?: 'compact' | 'exact'\n /**\n * Prunes the generated config to the classes found in your sources — the same files Tailwind scans, found the same way — so production bundles ship only the class groups and scale values the project uses. Lists composed of scanned candidates merge exactly like with the full generated config; retained validators may also match unscanned names.\n *\n * `true` (the default): prune in `next build`, serve the full config in `next dev`. `false`: never prune — for projects whose class names reach `twMerge` from outside the scanned sources *and* get their styles from somewhere else than this Tailwind build (server-delivered markup, module federation). The object form configures the details.\n */\n prune?: boolean | PruneOptions\n}\n\nexport interface PruneOptions {\n /** Prune production builds. Defaults to `true`. */\n build?: boolean\n /** Also prune in the dev server, for debugging differences between dev and build: every source edit that changes the used classes then regenerates the module. Defaults to `false`. */\n dev?: boolean\n /** Log one line per generation saying what pruning did. Defaults to `true`. */\n log?: boolean\n}\n\n/**\n * What the loader receives from the plugin through the bundler configuration. Plain data on purpose — Turbopack serializes loader options for its Rust side, so nothing here may be a function, a class instance, or `undefined` (absent keys are simply left out, see `serializableLoaderOptions`). The plugin resolves the user's options against the mode once so the loader never has to know the option surface.\n */\nexport type LoaderOptions = {\n /** `dev` for the dev server, `build` for `next build`: decides pruning and whether a generation failure fails the compilation (build) or falls back to the last good module (dev). */\n mode: 'dev' | 'build'\n prune: boolean\n log: boolean\n css?: string\n cacheSize?: number\n encoding?: 'compact' | 'exact'\n}\n\n/** Resolves the `prune` option's shorthand forms and defaults into the full object. */\nexport function resolvePruneOptions(option: TailwindMergeOptions['prune']): Required<PruneOptions> {\n if (option === false) {\n return { build: false, dev: false, log: false }\n }\n if (option === true || option === undefined) {\n return { build: true, dev: false, log: true }\n }\n return { build: option.build ?? true, dev: option.dev ?? false, log: option.log ?? true }\n}\n\n/** The loader options for one mode, with only the keys the user set — the loader applies the defaults of the underlying generator for the rest. */\nexport function resolveLoaderOptions(\n options: TailwindMergeOptions,\n mode: LoaderOptions['mode'],\n): LoaderOptions {\n const prune = resolvePruneOptions(options.prune)\n const resolved: LoaderOptions = {\n mode,\n prune: mode === 'build' ? prune.build : prune.dev,\n log: prune.log,\n }\n if (options.css !== undefined) {\n resolved.css = options.css\n }\n if (options.cacheSize !== undefined) {\n resolved.cacheSize = options.cacheSize\n }\n if (options.encoding !== undefined) {\n resolved.encoding = options.encoding\n }\n return resolved\n}\n\n/** The same options as a value the bundler configuration types accept: Next's loader option type has no room for `undefined`, which TypeScript reads into every optional key, so the object is rebuilt from its present keys. */\nexport function serializableLoaderOptions(\n options: LoaderOptions,\n): Record<string, string | number | boolean> {\n return Object.fromEntries(\n Object.entries(options).filter(\n (entry): entry is [string, string | number | boolean] => entry[1] !== undefined,\n ),\n )\n}\n","import { existsSync, realpathSync } from 'node:fs'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nimport type { NextConfig } from 'next'\n\nimport {\n type TailwindMergeOptions,\n resolveLoaderOptions,\n serializableLoaderOptions,\n} from './options'\n\nexport type { PruneOptions, TailwindMergeOptions } from './options'\n\n/** The function form of `next.config`, which Next.js calls with the current phase (see `next/constants`) and its default config. */\nexport type NextConfigFunction = (\n phase: string,\n context: { defaultConfig: NextConfig },\n) => NextConfig | Promise<NextConfig>\n\n/**\n * Configures tailwind-merge for the project's own Tailwind CSS in a Next.js app.\n *\n * Wrap the Next.js config with it and import from the runtime subpath: `import { twMerge } from '@tailwind-merge/next/runtime'`. While Next.js compiles the app, that import resolves to the module generated from the project's Tailwind theme by the inlined plugin core and configurator; outside Next.js it resolves to the real runtime.ts and serves default tailwind-merge behavior. Repository integration goals and invariants live in agents/next-plugin.md.\n *\n * Mechanics: neither Turbopack nor webpack offers virtual modules through the Next.js config, but both run webpack-style loaders, so the plugin registers a loader rule for this package's own `runtime.mjs` with both bundlers and lets the loader (src/loader.ts) replace the file's content with the generated module. The rules are keyed on the file name plus the legal comment inside the file, not on a path in node_modules, because Turbopack matches real paths — a symlinked install (`pnpm link`, a workspace) would otherwise fall through to the default config unnoticed. Dev and build get separate Turbopack rules through the built-in `development`/`production` conditions; the webpack hook reads the mode from its context. `transpilePackages` keeps the Pages Router's server bundle from externalizing the package: an externalized runtime would `require()` the on-disk fallback at request time while the client bundle carries the generated module, and the two would merge classes differently.\n *\n * The dev loop is deliberately quiet: the loader registers the files of the CSS configuration graph as dependencies (never the app's sources, unless `prune.dev` asks for it), so Next.js re-runs it only when one of those changes, and a regeneration that produces identical code leaves the bundlers' module hashes unchanged. Production builds additionally prune the config to the classes found in the project's sources (`prune` option).\n */\nexport function withTailwindMerge(\n nextConfig: NextConfigFunction,\n options?: TailwindMergeOptions,\n): NextConfigFunction\nexport function withTailwindMerge(\n nextConfig?: NextConfig,\n options?: TailwindMergeOptions,\n): NextConfig\nexport function withTailwindMerge(\n nextConfig: NextConfig | NextConfigFunction = {},\n options: TailwindMergeOptions = {},\n): NextConfig | NextConfigFunction {\n if (typeof nextConfig === 'function') {\n return async (phase, context) => applyToConfig(await nextConfig(phase, context), options)\n }\n return applyToConfig(nextConfig, options)\n}\n\ntype TurbopackRules = NonNullable<NonNullable<NextConfig['turbopack']>['rules']>\ntype TurbopackRuleCollection = TurbopackRules[string]\ntype TurbopackRuleItem = Exclude<TurbopackRuleCollection, readonly unknown[]>\n\nfunction applyToConfig(config: NextConfig, options: TailwindMergeOptions): NextConfig {\n warnWithoutCompilerConfig()\n const rules = config.turbopack?.rules ?? {}\n const existing = rules[RULE_GLOB]\n const existingItems =\n existing === undefined ? [] : Array.isArray(existing) ? existing : [existing]\n const transpilePackages = config.transpilePackages ?? []\n\n return {\n ...config,\n transpilePackages: transpilePackages.includes(PACKAGE_NAME)\n ? transpilePackages\n : [...transpilePackages, PACKAGE_NAME],\n turbopack: {\n ...config.turbopack,\n rules: {\n ...rules,\n // A user rule on the same glob keeps running; rules are evaluated in order, so the plugin's come last.\n [RULE_GLOB]: [...existingItems, ...turbopackRules(options)],\n },\n },\n webpack(webpackConfig, context) {\n webpackConfig.module.rules.push({\n test: isRuntimeModule,\n use: [\n {\n loader: LOADER_PATH,\n options: serializableLoaderOptions(\n resolveLoaderOptions(options, context.dev ? 'dev' : 'build'),\n ),\n },\n ],\n })\n return config.webpack ? config.webpack(webpackConfig, context) : webpackConfig\n },\n }\n}\n\nlet warnedWithoutCompilerConfig = false\n\n/**\n * Turbopack applies loader rules to code in node_modules only when the project has a `tsconfig.json` or `jsconfig.json` — even an empty one; the content does not matter (observed with Next.js 16.3, reproduced with a bare `*.mjs` rule). Without one, the runtime subpath would silently keep tailwind-merge's default behavior under Turbopack, so say so once per process. create-next-app writes one of the two files for every project, which keeps this rare. The working directory stands in for the project directory, which the config API does not expose.\n */\nfunction warnWithoutCompilerConfig() {\n if (warnedWithoutCompilerConfig) {\n return\n }\n warnedWithoutCompilerConfig = true\n const directory = process.cwd()\n if (\n !['tsconfig.json', 'jsconfig.json'].some((file) => existsSync(path.join(directory, file)))\n ) {\n console.warn(\n `[${PACKAGE_NAME}] No tsconfig.json or jsconfig.json found in ${directory}. Turbopack only applies the plugin's loader to installed packages when the project has one, so without it the runtime import keeps tailwind-merge's default configuration. Add an empty jsconfig.json if the project has no TypeScript configuration.`,\n )\n }\n}\n\n/** One rule per mode: Turbopack evaluates the plugin's config once for `next dev` and `next build` alike, so the mode has to be a rule condition rather than a value computed at config time. */\nfunction turbopackRules(options: TailwindMergeOptions): TurbopackRuleItem[] {\n return (['dev', 'build'] as const).map((mode) => ({\n condition: {\n all: [\n { path: RUNTIME_PATH_PATTERN },\n { content: RUNTIME_MARKER_PATTERN },\n mode === 'dev' ? 'development' : 'production',\n ],\n },\n loaders: [\n {\n loader: LOADER_PATH,\n options: serializableLoaderOptions(resolveLoaderOptions(options, mode)),\n },\n ],\n }))\n}\n\n/** webpack hands rules the resolved resource path, with symlinks resolved by default: compare against both spellings of this package's runtime file. */\nfunction isRuntimeModule(file: string): boolean {\n return file === RUNTIME_PATH || file === runtimeRealPath()\n}\n\nlet resolvedRuntimeRealPath: string | undefined\n\nfunction runtimeRealPath(): string {\n if (resolvedRuntimeRealPath === undefined) {\n try {\n resolvedRuntimeRealPath = realpathSync(RUNTIME_PATH)\n } catch {\n // The package's own runtime file is missing only in unbuilt source checkouts; the unresolved path keeps the comparison meaningful.\n resolvedRuntimeRealPath = RUNTIME_PATH\n }\n }\n return resolvedRuntimeRealPath\n}\n\nconst PACKAGE_NAME = '@tailwind-merge/next'\n\n/** Turbopack rule keys are file-name globs; the conditions below narrow the rule to this package's runtime file. */\nconst RULE_GLOB = '*.mjs'\n\n/** Turbopack matches project-relative real paths with forward slashes; only the file name is fixed across install layouts. */\nconst RUNTIME_PATH_PATTERN = /(^|\\/)runtime\\.mjs$/\n\n/** The legal comment at the top of src/runtime.ts, which the build preserves — the one part of the runtime file's content the rule can rely on across install layouts. */\nconst RUNTIME_MARKER_PATTERN = /@tailwind-merge\\/next runtime fallback/\n\n/** Both bundlers accept a loader as an absolute file path, which sidesteps loader-name resolution and the exports map. Built and source layouts agree on the relative location. */\nconst LOADER_PATH = fileURLToPath(new URL('./loader.mjs', import.meta.url))\n\nconst RUNTIME_PATH = fileURLToPath(new URL('./runtime.mjs', import.meta.url))\n"],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { t as LoaderOptions } from "./options-B1iOW6tJ.mjs";
|
|
2
|
+
//#region src/loader.d.ts
|
|
3
|
+
/** The slice of the webpack loader API this loader uses. Turbopack's loader runner provides the same members (verified against Next.js 16.3, where `addMissingDependency` is accepted but not acted on — the recovery directories the plugin core reports cover that case as context dependencies). */
|
|
4
|
+
interface LoaderContext {
|
|
5
|
+
/** The Next.js project directory. */
|
|
6
|
+
rootContext: string;
|
|
7
|
+
resourcePath: string;
|
|
8
|
+
getOptions(): LoaderOptions;
|
|
9
|
+
addDependency(file: string): void;
|
|
10
|
+
addContextDependency(directory: string): void;
|
|
11
|
+
addMissingDependency(file: string): void;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Replaces the content of this package's runtime module with the module generated from the project's Tailwind CSS. Registered by `withTailwindMerge` (src/index.ts) with both Turbopack and webpack; runs once per compilation environment (server components, server rendering, browser), which the per-process project state below turns into one generation.
|
|
15
|
+
*
|
|
16
|
+
* Every run registers the CSS configuration graph — entrypoint, imported stylesheets, `@config`/`@plugin` modules, and the directories a missing import would appear in — as dependencies, so the bundler re-runs the loader when any of them changes and `refresh()` decides whether that change regenerates the module. A regeneration with identical output changes nothing downstream: both bundlers hash module content. With pruning active (builds, or dev with `prune.dev`), the scanned source directories are context dependencies too, so a class-usage change re-prunes the retained classification, and Turbopack's persistent cache cannot serve a module pruned for other sources.
|
|
17
|
+
*
|
|
18
|
+
* Failure policy mirrors the Vite plugin's: a build fails on a configuration-generation failure (the loader rejects, which fails the compilation), while the dev server logs the error and keeps serving the last good module — or the default fallback before the first success. A project without a Tailwind root warns once and serves the default fallback as well. A failed source scan is not a generation failure: the module holds the full config and the warning says so.
|
|
19
|
+
*/
|
|
20
|
+
declare function tailwindMergeLoader(this: LoaderContext): Promise<string>;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { LoaderContext, tailwindMergeLoader as default };
|
|
23
|
+
//# sourceMappingURL=loader.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.d.mts","names":[],"sources":["../src/loader.ts"],"mappings":";;;UAciB;;EAEb;EACA;EACA,cAAc;EACd,cAAc;EACd,qBAAqB;EACrB,qBAAqB;;;;;;;;;iBAUK,oBAAoB,MAAM,gBAAgB"}
|