tailwind-preset-mantine 4.0.0-alpha.5 → 4.0.0

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/README.md CHANGED
@@ -70,7 +70,7 @@ Note that you don't have to import tailwind or mantine styles, this preset will
70
70
 
71
71
  ### Custom mantine theme
72
72
 
73
- If you have a custom mantine theme (<https://mantine.dev/theming/theme-object/>), the recommended setup is to generate a static stylesheet from your theme file and import that stylesheet after the preset.
73
+ If you have a custom mantine theme (<https://mantine.dev/theming/theme-object/>), the recommended setup is to generate a complete stylesheet from your theme file and import that stylesheet directly.
74
74
 
75
75
  1. Create a theme file (e.g., `mantine-theme.ts`):
76
76
 
@@ -109,6 +109,8 @@ export default theme;
109
109
 
110
110
  2. Configure the integration to generate a stylesheet from your Mantine theme.
111
111
 
112
+ By default, the generated file is written next to the theme file with the same basename and a `.css` extension. For example, `./src/mantine-theme.ts` generates `./src/mantine-theme.css`.
113
+
112
114
  PostCSS:
113
115
 
114
116
  ```js
@@ -118,8 +120,6 @@ export default {
118
120
  plugins: [
119
121
  mantineTheme({
120
122
  input: "./src/mantine-theme.ts",
121
- output: "./src/mantine-theme.css",
122
- format: "theme",
123
123
  }),
124
124
  ],
125
125
  };
@@ -135,8 +135,6 @@ export default defineConfig({
135
135
  plugins: [
136
136
  mantineTheme({
137
137
  input: "./src/mantine-theme.ts",
138
- output: "./src/mantine-theme.css",
139
- format: "theme",
140
138
  }),
141
139
  ],
142
140
  });
@@ -144,14 +142,21 @@ export default defineConfig({
144
142
 
145
143
  The Vite integration also watches local modules imported by your Mantine theme file, so updates to split files like `theme/colors.ts` or `theme/spacing.ts` trigger the generated stylesheet to update automatically.
146
144
 
147
- 3. Import the generated stylesheet after the preset.
145
+ 3. Import the generated stylesheet.
148
146
 
149
147
  ```css
150
- @import "tailwind-preset-mantine";
151
148
  @import "./mantine-theme.css";
152
149
  ```
153
150
 
154
- This keeps the default preset import path unchanged while still generating Tailwind theme variables from your merged Mantine theme during development and production builds. Your app and editor tooling both read the generated stylesheet, so custom classes stay available in builds and IDE autocomplete.
151
+ The generated stylesheet includes the default imports and your merged Mantine theme.
152
+
153
+ #### Integration options
154
+
155
+ | Option | Required | Default | Description |
156
+ |--------|----------|---------|-------------|
157
+ | `input` | Yes | – | Path to the Mantine theme source file |
158
+ | `output` | No | `input` basename with `.css` extension | Path to the generated stylesheet |
159
+ | `format` | No | `theme` | `theme` generates Tailwind aliases only; `standalone` generates Mantine variables plus Tailwind aliases |
155
160
 
156
161
  ### Standalone pages without MantineProvider
157
162
 
@@ -162,7 +167,6 @@ PostCSS or Vite:
162
167
  ```ts
163
168
  mantineTheme({
164
169
  input: "./src/mantine-theme.ts",
165
- output: "./src/mantine-theme.css",
166
170
  format: "standalone",
167
171
  });
168
172
  ```
@@ -180,18 +184,17 @@ If you already render `MantineProvider` on the same page, prefer the default `th
180
184
  If your setup does not use PostCSS or Vite, you can still generate the theme CSS with the CLI:
181
185
 
182
186
  ```bash
183
- npx tailwind-preset-mantine mantine-theme.ts -o theme.css
187
+ npx tailwind-preset-mantine mantine-theme.ts
184
188
  ```
185
189
 
186
190
  Options:
187
- - `-o, --output`: Output file name/location (default: "theme.css")
191
+ - `-o, --output`: Output file name/location (defaults to the input filename with a `.css` extension)
188
192
  - `--format theme|standalone`: Output either Tailwind aliases only (`theme`, default) or Mantine variables plus Tailwind aliases (`standalone`)
189
193
 
190
- Then import the generated file after the preset:
194
+ Then import the generated file:
191
195
 
192
196
  ```css
193
- @import "tailwind-preset-mantine";
194
- @import "./theme.css";
197
+ @import "./mantine-theme.css";
195
198
  ```
196
199
 
197
200
  Use `--format standalone` when generating CSS for pages that do not render `MantineProvider`.
package/package.json CHANGED
@@ -1,69 +1,69 @@
1
1
  {
2
- "name": "tailwind-preset-mantine",
3
- "version": "4.0.0-alpha.5",
4
- "description": "Integrate Mantine with Tailwind CSS",
5
- "keywords": [
6
- "mantine",
7
- "tailwind",
8
- "preset"
9
- ],
10
- "homepage": "https://github.com/songkeys/tailwind-preset-mantine#readme",
11
- "bugs": {
12
- "url": "https://github.com/songkeys/tailwind-preset-mantine/issues"
13
- },
14
- "repository": {
15
- "type": "git",
16
- "url": "git+https://github.com/songkeys/tailwind-preset-mantine.git"
17
- },
18
- "license": "MIT",
19
- "author": "Songkeys",
20
- "type": "module",
21
- "exports": {
22
- ".": "./src/index.css",
23
- "./theme.css": "./src/theme.css",
24
- "./postcss": {
25
- "types": "./src/integrations/postcss.d.ts",
26
- "default": "./src/integrations/postcss.js"
27
- },
28
- "./vite": {
29
- "types": "./src/integrations/vite.d.ts",
30
- "default": "./src/integrations/vite.js"
31
- }
32
- },
33
- "main": "src/index.css",
34
- "bin": {
35
- "tailwind-preset-mantine": "src/cli/index.js"
36
- },
37
- "files": [
38
- "src"
39
- ],
40
- "engines": {
41
- "node": ">=22.15.0"
42
- },
43
- "scripts": {
44
- "generate": "node scripts/generate.js",
45
- "lint": "biome check .",
46
- "lint:fix": "biome check . --write",
47
- "release": "bumpp",
48
- "test": "node --test"
49
- },
50
- "dependencies": {
51
- "es-module-lexer": "^2.0.0",
52
- "postcss": "^8.5.6",
53
- "tsx": "^4.21.0"
54
- },
55
- "devDependencies": {
56
- "@tailwindcss/postcss": "^4",
57
- "@tailwindcss/vite": "^4",
58
- "@biomejs/biome": "^2.4.10",
59
- "@mantine/core": "^9",
60
- "bumpp": "^11.0.1",
61
- "tailwindcss": "^4",
62
- "vite": "^7"
63
- },
64
- "peerDependencies": {
65
- "@mantine/core": "^7 || ^7 || ^9",
66
- "tailwindcss": "^4"
67
- },
68
- "packageManager": "pnpm@10.33.0"
69
- }
2
+ "name": "tailwind-preset-mantine",
3
+ "version": "4.0.0",
4
+ "description": "Integrate Mantine with Tailwind CSS",
5
+ "keywords": [
6
+ "mantine",
7
+ "tailwind",
8
+ "preset"
9
+ ],
10
+ "homepage": "https://github.com/songkeys/tailwind-preset-mantine#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/songkeys/tailwind-preset-mantine/issues"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/songkeys/tailwind-preset-mantine.git"
17
+ },
18
+ "license": "MIT",
19
+ "author": "Songkeys",
20
+ "type": "module",
21
+ "exports": {
22
+ ".": "./src/index.css",
23
+ "./theme.css": "./src/theme.css",
24
+ "./postcss": {
25
+ "types": "./src/integrations/postcss.d.ts",
26
+ "default": "./src/integrations/postcss.js"
27
+ },
28
+ "./vite": {
29
+ "types": "./src/integrations/vite.d.ts",
30
+ "default": "./src/integrations/vite.js"
31
+ }
32
+ },
33
+ "main": "src/index.css",
34
+ "bin": {
35
+ "tailwind-preset-mantine": "src/cli/index.js"
36
+ },
37
+ "files": [
38
+ "src"
39
+ ],
40
+ "engines": {
41
+ "node": ">=22.15.0"
42
+ },
43
+ "dependencies": {
44
+ "es-module-lexer": "^2.0.0",
45
+ "get-tsconfig": "^4.13.7",
46
+ "postcss": "^8.5.9",
47
+ "tsx": "^4.21.0"
48
+ },
49
+ "devDependencies": {
50
+ "@biomejs/biome": "^2.4.11",
51
+ "@mantine/core": "^9.0.1",
52
+ "@tailwindcss/postcss": "^4.2.2",
53
+ "@tailwindcss/vite": "^4.2.2",
54
+ "bumpp": "^11.0.1",
55
+ "tailwindcss": "^4.2.2",
56
+ "vite": "^8.0.8"
57
+ },
58
+ "peerDependencies": {
59
+ "@mantine/core": "^7 || ^8 || ^9",
60
+ "tailwindcss": "^4"
61
+ },
62
+ "scripts": {
63
+ "generate": "node scripts/generate.js",
64
+ "lint": "biome check .",
65
+ "lint:fix": "biome check . --write",
66
+ "release": "bumpp",
67
+ "test": "node --test"
68
+ }
69
+ }
package/src/cli/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { relative } from "node:path";
2
3
  import { parseArgs } from "node:util";
3
4
  import { validateOutputFormat, writeThemeOutput } from "../core/output.js";
4
5
 
@@ -9,7 +10,6 @@ const options = {
9
10
  output: {
10
11
  type: "string",
11
12
  short: "o",
12
- default: "theme.css",
13
13
  description: "Output file name",
14
14
  },
15
15
  format: {
@@ -41,7 +41,7 @@ try {
41
41
  }
42
42
 
43
43
  try {
44
- await writeThemeOutput(
44
+ const { outputPath } = await writeThemeOutput(
45
45
  {
46
46
  input: inputFile,
47
47
  output: outputFile,
@@ -50,7 +50,9 @@ try {
50
50
  { baseDir: pwd },
51
51
  );
52
52
 
53
- console.log(`Successfully generated ${outputFile}`);
53
+ console.log(
54
+ `Successfully generated ${relative(pwd, outputPath) || outputPath}`,
55
+ );
54
56
  } catch (error) {
55
57
  console.error("Error generating theme:", error.message);
56
58
  process.exit(1);
@@ -75,7 +75,6 @@ function generateStandaloneMantineVariables(theme = DEFAULT_THEME) {
75
75
  * @param {import("@mantine/core").MantineTheme} theme
76
76
  */
77
77
  export function generateTheme(theme = DEFAULT_THEME) {
78
- const isDefault = JSON.stringify(theme) === JSON.stringify(DEFAULT_THEME);
79
78
  const mergedTheme = mergeMantineTheme(DEFAULT_THEME, theme);
80
79
 
81
80
  const colorKeys = Object.keys(mergedTheme.colors ?? {});
@@ -91,12 +90,10 @@ export function generateTheme(theme = DEFAULT_THEME) {
91
90
  const defaultShadowKey = shadowKeys.includes("xs")
92
91
  ? "xs"
93
92
  : (shadowKeys[0] ?? "xs");
94
- const darkVariant = isDefault
95
- ? `@custom-variant dark (&:where(
93
+ const darkVariant = `@custom-variant dark (&:where(
96
94
  [data-mantine-color-scheme="dark"],
97
95
  [data-mantine-color-scheme="dark"] *
98
- ));`
99
- : "";
96
+ ));`;
100
97
 
101
98
  return `${AUTOGENERATED_COMMENT}${darkVariant}
102
99
 
@@ -263,3 +260,21 @@ export function generateStandaloneTheme(theme = DEFAULT_THEME) {
263
260
  return `${AUTOGENERATED_COMMENT}${standaloneMantineCSS}
264
261
  ${tailwindThemeCSS}`;
265
262
  }
263
+
264
+ /**
265
+ * @param {import("@mantine/core").MantineTheme} theme
266
+ * @param {"theme" | "standalone"} [format]
267
+ */
268
+ export function generateManagedStylesheet(
269
+ theme = DEFAULT_THEME,
270
+ format = "theme",
271
+ ) {
272
+ const themeCSS = stripAutogeneratedComment(
273
+ format === "standalone"
274
+ ? generateStandaloneTheme(theme)
275
+ : generateTheme(theme),
276
+ );
277
+
278
+ return `${generateDefaultImports()}
279
+ ${themeCSS}`;
280
+ }
@@ -1,35 +1,61 @@
1
- import { mkdir, readFile, writeFile } from "node:fs/promises";
2
- import { dirname, resolve } from "node:path";
1
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { basename, dirname, extname, join, resolve } from "node:path";
3
3
  import { fileURLToPath, pathToFileURL } from "node:url";
4
- import { generateStandaloneTheme, generateTheme } from "./generate.js";
4
+ import { createPathsMatcher, getTsconfig } from "get-tsconfig";
5
+ import { generateManagedStylesheet } from "./generate.js";
5
6
  import { collectThemeDependencies } from "./theme-dependencies.js";
6
7
  import { loadThemeFromFile } from "./theme-loader.js";
7
8
 
8
- const OUTPUT_FORMAT_GENERATORS = {
9
- theme: generateTheme,
10
- standalone: generateStandaloneTheme,
11
- };
9
+ const OUTPUT_FORMATS = ["theme", "standalone"];
10
+ const PACKAGE_JSON_FILENAMES = ["package.json"];
11
+ const THEME_SOURCE_EXTENSIONS = [
12
+ ".js",
13
+ ".jsx",
14
+ ".mjs",
15
+ ".cjs",
16
+ ".json",
17
+ ".ts",
18
+ ".tsx",
19
+ ".mts",
20
+ ".cts",
21
+ ];
22
+ const JSON_FILE_CACHE = new Map();
23
+ const TSCONFIG_DISCOVERY_CACHE = new Map();
24
+ const TSCONFIG_FS_CACHE = new Map();
25
+ const TSCONFIG_PATHS_MATCHER_CACHE = new Map();
12
26
 
13
27
  function validateOptions(options) {
14
28
  if (!options?.input) {
15
29
  throw new Error("Missing required `input` option.");
16
30
  }
17
-
18
- if (!options?.output) {
19
- throw new Error("Missing required `output` option.");
20
- }
21
31
  }
22
32
 
23
33
  export function validateOutputFormat(format = "theme") {
24
- if (!(format in OUTPUT_FORMAT_GENERATORS)) {
34
+ if (!OUTPUT_FORMATS.includes(format)) {
25
35
  throw new Error(
26
- `Invalid output format: ${format}. Expected one of: ${Object.keys(OUTPUT_FORMAT_GENERATORS).join(", ")}`,
36
+ `Invalid output format: ${format}. Expected one of: ${OUTPUT_FORMATS.join(", ")}`,
27
37
  );
28
38
  }
29
39
  }
30
40
 
31
41
  /**
32
- * @param {{ input: string, output: string, format?: "theme" | "standalone" }} options
42
+ * @param {string} inputPath
43
+ * @param {string | undefined} output
44
+ * @param {string} baseDir
45
+ */
46
+ function resolveOutputPath(inputPath, output, baseDir) {
47
+ if (output) {
48
+ return resolve(baseDir, output);
49
+ }
50
+
51
+ const inputDirectory = dirname(inputPath);
52
+ const inputFilename = basename(inputPath, extname(inputPath));
53
+
54
+ return join(inputDirectory, `${inputFilename}.css`);
55
+ }
56
+
57
+ /**
58
+ * @param {{ input: string, output?: string, format?: "theme" | "standalone" }} options
33
59
  * @param {string} [baseDir]
34
60
  */
35
61
  export function resolveThemeOutputOptions(options, baseDir = process.cwd()) {
@@ -37,10 +63,11 @@ export function resolveThemeOutputOptions(options, baseDir = process.cwd()) {
37
63
 
38
64
  const format = options.format ?? "theme";
39
65
  validateOutputFormat(format);
66
+ const inputPath = resolve(baseDir, options.input);
40
67
 
41
68
  return {
42
- inputPath: resolve(baseDir, options.input),
43
- outputPath: resolve(baseDir, options.output),
69
+ inputPath,
70
+ outputPath: resolveOutputPath(inputPath, options.output, baseDir),
44
71
  format,
45
72
  };
46
73
  }
@@ -50,23 +77,334 @@ export function resolveThemeOutputOptions(options, baseDir = process.cwd()) {
50
77
  * @param {string} importer
51
78
  */
52
79
  async function resolveThemeImport(specifier, importer) {
53
- if (!(specifier.startsWith(".") || specifier.startsWith("file:"))) {
54
- if (specifier.startsWith("/")) {
55
- return specifier;
80
+ if (specifier.startsWith("/")) {
81
+ return specifier;
82
+ }
83
+
84
+ try {
85
+ if (specifier.startsWith("file:")) {
86
+ return fileURLToPath(specifier);
87
+ }
88
+
89
+ if (specifier.startsWith(".")) {
90
+ return fileURLToPath(new URL(specifier, pathToFileURL(importer)));
91
+ }
92
+ } catch {
93
+ // Fall through to config-based alias resolution.
94
+ }
95
+
96
+ if (specifier.startsWith("#")) {
97
+ const packageJsonPath = await findClosestConfig(
98
+ dirname(importer),
99
+ PACKAGE_JSON_FILENAMES,
100
+ );
101
+ const packageImport = await resolvePackageImport(
102
+ specifier,
103
+ packageJsonPath,
104
+ );
105
+
106
+ if (packageImport) {
107
+ return packageImport;
56
108
  }
109
+ }
110
+
111
+ return resolveTsconfigImport(specifier, dirname(importer));
112
+ }
113
+
114
+ function matchSpecifierPattern(specifier, pattern) {
115
+ const wildcardIndex = pattern.indexOf("*");
116
+
117
+ if (wildcardIndex === -1) {
118
+ return specifier === pattern ? "" : null;
119
+ }
57
120
 
121
+ const prefix = pattern.slice(0, wildcardIndex);
122
+ const suffix = pattern.slice(wildcardIndex + 1);
123
+
124
+ if (!specifier.startsWith(prefix) || !specifier.endsWith(suffix)) {
58
125
  return null;
59
126
  }
60
127
 
128
+ return specifier.slice(prefix.length, specifier.length - suffix.length);
129
+ }
130
+
131
+ function flattenImportTargets(target) {
132
+ if (typeof target === "string") {
133
+ return [target];
134
+ }
135
+
136
+ if (Array.isArray(target)) {
137
+ return target.flatMap((item) => flattenImportTargets(item));
138
+ }
139
+
140
+ if (!target || typeof target !== "object") {
141
+ return [];
142
+ }
143
+
144
+ const orderedConditions = ["default", "import", "node"];
145
+ const orderedTargets = [];
146
+
147
+ for (const condition of orderedConditions) {
148
+ if (condition in target) {
149
+ orderedTargets.push(target[condition]);
150
+ }
151
+ }
152
+
153
+ for (const [condition, value] of Object.entries(target)) {
154
+ if (!orderedConditions.includes(condition)) {
155
+ orderedTargets.push(value);
156
+ }
157
+ }
158
+
159
+ return orderedTargets.flatMap((value) => flattenImportTargets(value));
160
+ }
161
+
162
+ function getMappedImportCandidates(
163
+ specifier,
164
+ mappings,
165
+ baseDirectory,
166
+ localOnly,
167
+ ) {
168
+ if (!mappings || typeof mappings !== "object") {
169
+ return [];
170
+ }
171
+
172
+ for (const [pattern, target] of Object.entries(mappings)) {
173
+ const match = matchSpecifierPattern(specifier, pattern);
174
+
175
+ if (match == null) {
176
+ continue;
177
+ }
178
+
179
+ const resolvedCandidates = [];
180
+
181
+ for (const candidate of flattenImportTargets(target)) {
182
+ if (typeof candidate !== "string") {
183
+ continue;
184
+ }
185
+
186
+ if (localOnly && !candidate.startsWith("./")) {
187
+ continue;
188
+ }
189
+
190
+ const resolvedTarget = candidate.includes("*")
191
+ ? candidate.replace("*", match)
192
+ : candidate;
193
+
194
+ if (
195
+ /^[a-z]+:/i.test(resolvedTarget) &&
196
+ !resolvedTarget.startsWith("file:")
197
+ ) {
198
+ continue;
199
+ }
200
+
201
+ resolvedCandidates.push(
202
+ resolvedTarget.startsWith("file:")
203
+ ? fileURLToPath(resolvedTarget)
204
+ : resolve(baseDirectory, resolvedTarget),
205
+ );
206
+ }
207
+
208
+ return resolvedCandidates;
209
+ }
210
+
211
+ return [];
212
+ }
213
+
214
+ async function pathExists(path) {
61
215
  try {
62
- return fileURLToPath(new URL(specifier, pathToFileURL(importer)));
216
+ await access(path);
217
+ return true;
63
218
  } catch {
219
+ return false;
220
+ }
221
+ }
222
+
223
+ /**
224
+ * @param {string} path
225
+ */
226
+ async function readJsonFile(path) {
227
+ if (JSON_FILE_CACHE.has(path)) {
228
+ return JSON_FILE_CACHE.get(path);
229
+ }
230
+
231
+ const json = await readFile(path, "utf8")
232
+ .then((source) => JSON.parse(source))
233
+ .catch(() => null);
234
+
235
+ JSON_FILE_CACHE.set(path, json);
236
+ return json;
237
+ }
238
+
239
+ /**
240
+ * @param {string} startDirectory
241
+ * @param {string[]} filenames
242
+ */
243
+ async function findClosestConfig(startDirectory, filenames) {
244
+ let currentDirectory = startDirectory;
245
+
246
+ while (true) {
247
+ for (const filename of filenames) {
248
+ const candidate = join(currentDirectory, filename);
249
+
250
+ if (await pathExists(candidate)) {
251
+ return candidate;
252
+ }
253
+ }
254
+
255
+ const parentDirectory = dirname(currentDirectory);
256
+
257
+ if (parentDirectory === currentDirectory) {
258
+ return null;
259
+ }
260
+
261
+ currentDirectory = parentDirectory;
262
+ }
263
+ }
264
+
265
+ /**
266
+ * @param {string} specifier
267
+ * @param {string | null} packageJsonPath
268
+ */
269
+ async function resolvePackageImport(specifier, packageJsonPath) {
270
+ if (!packageJsonPath) {
64
271
  return null;
65
272
  }
273
+
274
+ const packageJson = await readJsonFile(packageJsonPath);
275
+ return resolveFirstExistingThemeSource(
276
+ getMappedImportCandidates(
277
+ specifier,
278
+ packageJson?.imports,
279
+ dirname(packageJsonPath),
280
+ true,
281
+ ),
282
+ );
283
+ }
284
+
285
+ function getTsconfigPathsMatcher(tsconfigPath) {
286
+ if (TSCONFIG_PATHS_MATCHER_CACHE.has(tsconfigPath.path)) {
287
+ return TSCONFIG_PATHS_MATCHER_CACHE.get(tsconfigPath.path);
288
+ }
289
+
290
+ let matcher = null;
291
+
292
+ try {
293
+ matcher = createPathsMatcher(tsconfigPath);
294
+ } catch {
295
+ // Ignore unreadable or invalid configs and continue without alias support.
296
+ }
297
+
298
+ TSCONFIG_PATHS_MATCHER_CACHE.set(tsconfigPath.path, matcher);
299
+ return matcher;
300
+ }
301
+
302
+ function getConfigDistance(startDirectory, configPath) {
303
+ let currentDirectory = resolve(startDirectory);
304
+ const configDirectory = dirname(configPath);
305
+ let distance = 0;
306
+
307
+ while (true) {
308
+ if (currentDirectory === configDirectory) {
309
+ return distance;
310
+ }
311
+
312
+ const parentDirectory = dirname(currentDirectory);
313
+
314
+ if (parentDirectory === currentDirectory) {
315
+ return Number.POSITIVE_INFINITY;
316
+ }
317
+
318
+ currentDirectory = parentDirectory;
319
+ distance += 1;
320
+ }
321
+ }
322
+
323
+ function getClosestTsconfigResult(startDirectory) {
324
+ if (TSCONFIG_DISCOVERY_CACHE.has(startDirectory)) {
325
+ return TSCONFIG_DISCOVERY_CACHE.get(startDirectory);
326
+ }
327
+
328
+ const tsconfigResult = getTsconfig(
329
+ startDirectory,
330
+ "tsconfig.json",
331
+ TSCONFIG_FS_CACHE,
332
+ );
333
+ const jsconfigResult = getTsconfig(
334
+ startDirectory,
335
+ "jsconfig.json",
336
+ TSCONFIG_FS_CACHE,
337
+ );
338
+
339
+ let closestResult = tsconfigResult ?? jsconfigResult;
340
+
341
+ if (tsconfigResult && jsconfigResult) {
342
+ const tsconfigDistance = getConfigDistance(
343
+ startDirectory,
344
+ tsconfigResult.path,
345
+ );
346
+ const jsconfigDistance = getConfigDistance(
347
+ startDirectory,
348
+ jsconfigResult.path,
349
+ );
350
+
351
+ closestResult =
352
+ jsconfigDistance < tsconfigDistance ? jsconfigResult : tsconfigResult;
353
+ }
354
+
355
+ TSCONFIG_DISCOVERY_CACHE.set(startDirectory, closestResult);
356
+ return closestResult;
357
+ }
358
+
359
+ function getThemeSourceCandidates(path) {
360
+ const candidates = [path];
361
+
362
+ if (extname(path)) {
363
+ return candidates;
364
+ }
365
+
366
+ for (const extension of THEME_SOURCE_EXTENSIONS) {
367
+ candidates.push(`${path}${extension}`);
368
+ }
369
+
370
+ for (const extension of THEME_SOURCE_EXTENSIONS) {
371
+ candidates.push(join(path, `index${extension}`));
372
+ }
373
+
374
+ return candidates;
375
+ }
376
+
377
+ async function resolveFirstExistingThemeSource(candidatePaths) {
378
+ for (const candidatePath of candidatePaths) {
379
+ for (const candidate of getThemeSourceCandidates(candidatePath)) {
380
+ if (await pathExists(candidate)) {
381
+ return candidate;
382
+ }
383
+ }
384
+ }
385
+
386
+ return null;
66
387
  }
67
388
 
68
389
  /**
69
- * @param {{ input: string, output: string, format?: "theme" | "standalone" }} options
390
+ * @param {string} specifier
391
+ * @param {string} startDirectory
392
+ */
393
+ async function resolveTsconfigImport(specifier, startDirectory) {
394
+ const tsconfigResult = getClosestTsconfigResult(startDirectory);
395
+
396
+ if (!tsconfigResult) {
397
+ return null;
398
+ }
399
+
400
+ const pathsMatcher = getTsconfigPathsMatcher(tsconfigResult);
401
+ return pathsMatcher
402
+ ? resolveFirstExistingThemeSource(pathsMatcher(specifier))
403
+ : null;
404
+ }
405
+
406
+ /**
407
+ * @param {{ input: string, output?: string, format?: "theme" | "standalone" }} options
70
408
  * @param {{ baseDir?: string, resolveImport?: (specifier: string, importer: string) => Promise<string | null> }} [runtimeOptions]
71
409
  */
72
410
  export async function buildThemeOutput(options, runtimeOptions = {}) {
@@ -76,12 +414,9 @@ export async function buildThemeOutput(options, runtimeOptions = {}) {
76
414
  options,
77
415
  baseDir,
78
416
  );
79
- const { absolutePath, theme } = await loadThemeFromFile(inputPath);
80
- const css = OUTPUT_FORMAT_GENERATORS[format](theme);
81
- const dependencies = await collectThemeDependencies(
82
- absolutePath,
83
- resolveImport,
84
- );
417
+ const dependencies = await collectThemeDependencies(inputPath, resolveImport);
418
+ const { absolutePath, theme } = await loadThemeFromFile(inputPath, baseDir);
419
+ const css = generateManagedStylesheet(theme, format);
85
420
 
86
421
  return {
87
422
  css,
@@ -93,7 +428,7 @@ export async function buildThemeOutput(options, runtimeOptions = {}) {
93
428
  }
94
429
 
95
430
  /**
96
- * @param {{ input: string, output: string, format?: "theme" | "standalone" }} options
431
+ * @param {{ input: string, output?: string, format?: "theme" | "standalone" }} options
97
432
  * @param {{ baseDir?: string, resolveImport?: (specifier: string, importer: string) => Promise<string | null> }} [runtimeOptions]
98
433
  */
99
434
  export async function writeThemeOutput(options, runtimeOptions = {}) {
@@ -1,5 +1,5 @@
1
- import { readFile } from "node:fs/promises";
2
- import { extname } from "node:path";
1
+ import { access, readFile } from "node:fs/promises";
2
+ import { extname, join } from "node:path";
3
3
  import { init, parse } from "es-module-lexer";
4
4
 
5
5
  const THEME_SOURCE_EXTENSIONS = new Set([
@@ -7,11 +7,78 @@ const THEME_SOURCE_EXTENSIONS = new Set([
7
7
  ".jsx",
8
8
  ".mjs",
9
9
  ".cjs",
10
+ ".json",
10
11
  ".ts",
11
12
  ".tsx",
12
13
  ".mts",
13
14
  ".cts",
14
15
  ]);
16
+ const COMMONJS_REQUIRE_PATTERN =
17
+ /\brequire\s*\(\s*(['"`])([^'"`\n\r]+)\1\s*\)/g;
18
+
19
+ function normalizeResolvedPath(resolved) {
20
+ return resolved.split("?", 1)[0]?.split("#", 1)[0] ?? resolved;
21
+ }
22
+
23
+ async function fileExists(file) {
24
+ try {
25
+ await access(file);
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ async function resolveThemeSourceDependency(
33
+ specifier,
34
+ importer,
35
+ resolveImport,
36
+ ) {
37
+ const resolved = await resolveImport(specifier, importer);
38
+
39
+ if (resolved == null || resolved.startsWith("\0")) {
40
+ return null;
41
+ }
42
+
43
+ const normalized = normalizeResolvedPath(resolved);
44
+ const extension = extname(normalized);
45
+
46
+ if (THEME_SOURCE_EXTENSIONS.has(extension)) {
47
+ return normalized;
48
+ }
49
+
50
+ if (extension) {
51
+ return null;
52
+ }
53
+
54
+ for (const candidateExtension of THEME_SOURCE_EXTENSIONS) {
55
+ const candidate = `${normalized}${candidateExtension}`;
56
+
57
+ if (await fileExists(candidate)) {
58
+ return candidate;
59
+ }
60
+ }
61
+
62
+ for (const candidateExtension of THEME_SOURCE_EXTENSIONS) {
63
+ const candidate = join(normalized, `index${candidateExtension}`);
64
+
65
+ if (await fileExists(candidate)) {
66
+ return candidate;
67
+ }
68
+ }
69
+
70
+ return null;
71
+ }
72
+
73
+ function collectRequireSpecifiers(source) {
74
+ const specifiers = [];
75
+
76
+ for (const match of source.matchAll(COMMONJS_REQUIRE_PATTERN)) {
77
+ specifiers.push(match[2]);
78
+ }
79
+
80
+ return specifiers;
81
+ }
15
82
 
16
83
  /**
17
84
  * @param {string} entryFile
@@ -32,26 +99,33 @@ export async function collectThemeDependencies(entryFile, resolveImport) {
32
99
 
33
100
  const source = await readFile(file, "utf8");
34
101
  const [imports] = parse(source);
102
+ const specifiers = new Set();
35
103
 
36
104
  for (const record of imports) {
37
- if (record.d !== -1 || record.n == null) {
105
+ if (record.n == null) {
38
106
  continue;
39
107
  }
40
108
 
41
- const resolved = await resolveImport(record.n, file);
109
+ specifiers.add(record.n);
110
+ }
42
111
 
43
- if (resolved == null || resolved.startsWith("\0")) {
44
- continue;
45
- }
112
+ for (const specifier of collectRequireSpecifiers(source)) {
113
+ specifiers.add(specifier);
114
+ }
46
115
 
47
- const normalized = resolved.split("?", 1)[0];
116
+ for (const specifier of specifiers) {
117
+ const dependency = await resolveThemeSourceDependency(
118
+ specifier,
119
+ file,
120
+ resolveImport,
121
+ );
48
122
 
49
- if (!THEME_SOURCE_EXTENSIONS.has(extname(normalized))) {
123
+ if (dependency == null) {
50
124
  continue;
51
125
  }
52
126
 
53
- dependencies.add(normalized);
54
- await visit(normalized);
127
+ dependencies.add(dependency);
128
+ await visit(dependency);
55
129
  }
56
130
  }
57
131
 
@@ -1,9 +1,14 @@
1
- import "tsx";
2
- import { stat } from "node:fs/promises";
1
+ import { execFile as execFileCallback } from "node:child_process";
3
2
  import * as nodeModule from "node:module";
4
3
  import { resolve } from "node:path";
5
- import { pathToFileURL } from "node:url";
6
-
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import { promisify } from "node:util";
6
+
7
+ const execFile = promisify(execFileCallback);
8
+ const THIS_FILE = fileURLToPath(import.meta.url);
9
+ const CHILD_RESULT_MARKER = "__TWPM_THEME_RESULT__";
10
+ const require = nodeModule.createRequire(import.meta.url);
11
+ const TSX_LOADER_PATH = require.resolve("tsx");
7
12
  const STYLE_EXTENSIONS = [
8
13
  ".css",
9
14
  ".scss",
@@ -66,7 +71,6 @@ function installThemeImportHooks() {
66
71
 
67
72
  themeImportHooksInstalled = true;
68
73
 
69
- const require = nodeModule.createRequire(import.meta.url);
70
74
  const extensions = require.extensions;
71
75
 
72
76
  for (const extension of STYLE_EXTENSIONS) {
@@ -112,13 +116,11 @@ function installThemeImportHooks() {
112
116
  * @param {string} themePath
113
117
  * @param {string} baseDir
114
118
  */
115
- export async function loadThemeFromFile(themePath, baseDir = process.cwd()) {
119
+ async function loadThemeFromFileInProcess(themePath, baseDir = process.cwd()) {
116
120
  installThemeImportHooks();
117
121
 
118
122
  const absolutePath = resolve(baseDir, themePath);
119
- const themeURL = pathToFileURL(absolutePath);
120
- const { mtimeMs } = await stat(absolutePath);
121
- const themeModule = await import(`${themeURL.href}?t=${mtimeMs}`);
123
+ const themeModule = await import(pathToFileURL(absolutePath).href);
122
124
  const theme = unwrapThemeExport(themeModule);
123
125
 
124
126
  if (!theme) {
@@ -129,3 +131,38 @@ export async function loadThemeFromFile(themePath, baseDir = process.cwd()) {
129
131
 
130
132
  return { absolutePath, theme };
131
133
  }
134
+
135
+ /**
136
+ * @param {string} themePath
137
+ * @param {string} baseDir
138
+ */
139
+ export async function loadThemeFromFile(themePath, baseDir = process.cwd()) {
140
+ const { stdout } = await execFile(
141
+ process.execPath,
142
+ ["--import", TSX_LOADER_PATH, THIS_FILE, "--child", themePath, baseDir],
143
+ {
144
+ cwd: resolve(baseDir),
145
+ maxBuffer: 5 * 1024 * 1024,
146
+ },
147
+ );
148
+ const markerIndex = stdout.lastIndexOf(CHILD_RESULT_MARKER);
149
+
150
+ if (markerIndex === -1) {
151
+ throw new Error("Theme loader child process did not return a result.");
152
+ }
153
+
154
+ return JSON.parse(stdout.slice(markerIndex + CHILD_RESULT_MARKER.length));
155
+ }
156
+
157
+ if (process.argv[1] === THIS_FILE && process.argv[2] === "--child") {
158
+ const themePath = process.argv[3];
159
+ const baseDir = process.argv[4] ?? process.cwd();
160
+
161
+ try {
162
+ const result = await loadThemeFromFileInProcess(themePath, baseDir);
163
+ process.stdout.write(`${CHILD_RESULT_MARKER}${JSON.stringify(result)}`);
164
+ } catch (error) {
165
+ console.error(error);
166
+ process.exitCode = 1;
167
+ }
168
+ }
@@ -1,6 +1,6 @@
1
1
  export interface MantineThemePluginOptions {
2
2
  input: string;
3
- output: string;
3
+ output?: string;
4
4
  format?: "theme" | "standalone";
5
5
  }
6
6
 
@@ -1,9 +1,11 @@
1
+ import { access } from "node:fs/promises";
2
+ import { dirname, resolve } from "node:path";
1
3
  import { writeThemeOutput } from "../core/output.js";
2
4
 
3
5
  /**
4
6
  * @typedef {{
5
7
  * input: string;
6
- * output: string;
8
+ * output?: string;
7
9
  * format?: "theme" | "standalone";
8
10
  * }} MantineThemePluginOptions
9
11
  */
@@ -12,11 +14,48 @@ import { writeThemeOutput } from "../core/output.js";
12
14
  * @param {MantineThemePluginOptions} options
13
15
  */
14
16
  function mantineTheme(options) {
17
+ async function resolveBaseDir(result) {
18
+ const candidates = [];
19
+ const cwd = result.opts.cwd ?? process.cwd();
20
+
21
+ if (result.opts.from) {
22
+ let current = resolve(cwd, result.opts.from);
23
+ current = dirname(current);
24
+
25
+ while (true) {
26
+ candidates.push(current);
27
+ const parent = dirname(current);
28
+
29
+ if (parent === current) {
30
+ break;
31
+ }
32
+
33
+ current = parent;
34
+ }
35
+ }
36
+
37
+ candidates.push(cwd);
38
+
39
+ const dedupedCandidates = [...new Set(candidates)];
40
+
41
+ for (const baseDir of dedupedCandidates) {
42
+ try {
43
+ await access(resolve(baseDir, options.input));
44
+ return baseDir;
45
+ } catch {
46
+ // Try the next candidate directory.
47
+ }
48
+ }
49
+
50
+ return cwd;
51
+ }
52
+
15
53
  return {
16
54
  postcssPlugin: "tailwind-preset-mantine",
17
55
  async Once(_, { result }) {
56
+ const baseDir = await resolveBaseDir(result);
18
57
  const { dependencies } = await writeThemeOutput(options, {
19
- baseDir: process.cwd(),
58
+ baseDir,
20
59
  });
21
60
 
22
61
  for (const file of dependencies) {
@@ -1,6 +1,6 @@
1
1
  export interface MantineThemePluginOptions {
2
2
  input: string;
3
- output: string;
3
+ output?: string;
4
4
  format?: "theme" | "standalone";
5
5
  }
6
6
 
@@ -3,7 +3,7 @@ import { writeThemeOutput } from "../core/output.js";
3
3
  /**
4
4
  * @typedef {{
5
5
  * input: string;
6
- * output: string;
6
+ * output?: string;
7
7
  * format?: "theme" | "standalone";
8
8
  * }} MantineThemePluginOptions
9
9
  */
@@ -16,22 +16,37 @@ export default function mantineTheme(options) {
16
16
  let outputPath = "";
17
17
  let dependencyFiles = new Set();
18
18
  let generatePromise = null;
19
+ let pendingRegeneration = false;
19
20
 
20
- async function generateThemeOutput() {
21
- if (!generatePromise) {
22
- generatePromise = writeThemeOutput(options, { baseDir: root }).finally(
23
- () => {
24
- generatePromise = null;
25
- },
26
- );
27
- }
28
-
29
- const result = await generatePromise;
21
+ async function runGeneration() {
22
+ const result = await writeThemeOutput(options, { baseDir: root });
30
23
  outputPath = result.outputPath;
31
24
  dependencyFiles = new Set(result.dependencies);
32
25
  return result;
33
26
  }
34
27
 
28
+ async function generateThemeOutput({ queueNextRun = false } = {}) {
29
+ if (generatePromise) {
30
+ pendingRegeneration ||= queueNextRun;
31
+ return generatePromise;
32
+ }
33
+
34
+ generatePromise = (async () => {
35
+ let result;
36
+
37
+ do {
38
+ pendingRegeneration = false;
39
+ result = await runGeneration();
40
+ } while (pendingRegeneration);
41
+
42
+ return result;
43
+ })().finally(() => {
44
+ generatePromise = null;
45
+ });
46
+
47
+ return generatePromise;
48
+ }
49
+
35
50
  return {
36
51
  name: "tailwind-preset-mantine",
37
52
  async configResolved(config) {
@@ -45,8 +60,8 @@ export default function mantineTheme(options) {
45
60
  }
46
61
  },
47
62
  configureServer(server) {
48
- const refresh = async () => {
49
- const result = await generateThemeOutput();
63
+ const refresh = async (options = undefined) => {
64
+ const result = await generateThemeOutput(options);
50
65
  server.watcher.add([...result.dependencies]);
51
66
  };
52
67
 
@@ -55,7 +70,7 @@ export default function mantineTheme(options) {
55
70
  return;
56
71
  }
57
72
 
58
- await refresh();
73
+ await refresh({ queueNextRun: true });
59
74
  };
60
75
 
61
76
  server.watcher.on("change", handleFileChange);