tailwind-preset-mantine 4.0.0-alpha.6 → 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
@@ -194,7 +194,7 @@ Options:
194
194
  Then import the generated file:
195
195
 
196
196
  ```css
197
- @import "./theme.css";
197
+ @import "./mantine-theme.css";
198
198
  ```
199
199
 
200
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.6",
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
+ }
@@ -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
 
@@ -1,11 +1,28 @@
1
- import { mkdir, readFile, writeFile } from "node:fs/promises";
1
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { basename, dirname, extname, join, resolve } from "node:path";
3
3
  import { fileURLToPath, pathToFileURL } from "node:url";
4
+ import { createPathsMatcher, getTsconfig } from "get-tsconfig";
4
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
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();
9
26
 
10
27
  function validateOptions(options) {
11
28
  if (!options?.input) {
@@ -60,19 +77,330 @@ export function resolveThemeOutputOptions(options, baseDir = process.cwd()) {
60
77
  * @param {string} importer
61
78
  */
62
79
  async function resolveThemeImport(specifier, importer) {
63
- if (!(specifier.startsWith(".") || specifier.startsWith("file:"))) {
64
- if (specifier.startsWith("/")) {
65
- 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;
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
+ }
120
+
121
+ const prefix = pattern.slice(0, wildcardIndex);
122
+ const suffix = pattern.slice(wildcardIndex + 1);
123
+
124
+ if (!specifier.startsWith(prefix) || !specifier.endsWith(suffix)) {
125
+ return null;
126
+ }
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) {
215
+ try {
216
+ await access(path);
217
+ return true;
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;
66
259
  }
67
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) {
68
271
  return null;
69
272
  }
70
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
+
71
292
  try {
72
- return fileURLToPath(new URL(specifier, pathToFileURL(importer)));
293
+ matcher = createPathsMatcher(tsconfigPath);
73
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;
387
+ }
388
+
389
+ /**
390
+ * @param {string} specifier
391
+ * @param {string} startDirectory
392
+ */
393
+ async function resolveTsconfigImport(specifier, startDirectory) {
394
+ const tsconfigResult = getClosestTsconfigResult(startDirectory);
395
+
396
+ if (!tsconfigResult) {
74
397
  return null;
75
398
  }
399
+
400
+ const pathsMatcher = getTsconfigPathsMatcher(tsconfigResult);
401
+ return pathsMatcher
402
+ ? resolveFirstExistingThemeSource(pathsMatcher(specifier))
403
+ : null;
76
404
  }
77
405
 
78
406
  /**
@@ -86,12 +414,9 @@ export async function buildThemeOutput(options, runtimeOptions = {}) {
86
414
  options,
87
415
  baseDir,
88
416
  );
89
- const { absolutePath, theme } = await loadThemeFromFile(inputPath);
417
+ const dependencies = await collectThemeDependencies(inputPath, resolveImport);
418
+ const { absolutePath, theme } = await loadThemeFromFile(inputPath, baseDir);
90
419
  const css = generateManagedStylesheet(theme, format);
91
- const dependencies = await collectThemeDependencies(
92
- absolutePath,
93
- resolveImport,
94
- );
95
420
 
96
421
  return {
97
422
  css,
@@ -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,3 +1,5 @@
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
  /**
@@ -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) {
@@ -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);