@mochi-css/next 2.0.0 → 3.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 ADDED
@@ -0,0 +1,145 @@
1
+ # 🧁 Mochi-CSS/next
2
+
3
+ This package is part of the [Mochi-CSS project](https://github.com/Niikelion/mochi-css).
4
+ It integrates compile-time CSS-in-JS into your Next.js builds.
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ npm i @mochi-css/vanilla @mochi-css/react
10
+ npm i -D @mochi-css/postcss @mochi-css/next
11
+ ```
12
+
13
+ > `@mochi-css/builder` and `@mochi-css/config` install transitively and do not need to be listed explicitly.
14
+
15
+ ---
16
+
17
+ ## How It Works
18
+
19
+ 1. The [PostCSS plugin](../postcss/README.md) extracts styles from your source files and generates CSS.
20
+ 2. The Next.js loader reads the style manifest if the postcss plugin generated one and injects import to in-flight generated CSS modules.
21
+
22
+ ---
23
+
24
+ ## Setup
25
+
26
+ ### 1. `mochi.config.ts`
27
+
28
+ Create a config file in your project root. Set `tmpDir` to enable CSS splitting.
29
+
30
+ ```typescript
31
+ // mochi.config.ts
32
+ import { defineConfig } from "@mochi-css/config"
33
+
34
+ export default defineConfig({
35
+ tmpDir: ".mochi",
36
+ })
37
+ ```
38
+
39
+ See [`@mochi-css/config`](../config/README.md) for the full list of shared options.
40
+
41
+ ### 2. `postcss.config.js`
42
+
43
+ Add the PostCSS plugin:
44
+
45
+ ```js
46
+ // postcss.config.js
47
+ module.exports = {
48
+ plugins: {
49
+ '@mochi-css/postcss': {}
50
+ }
51
+ }
52
+ ```
53
+
54
+ The plugin reads `tmpDir` from `mochi.config.ts` automatically - no need to repeat it here.
55
+ See [`@mochi-css/postcss`](../postcss/README.md) for PostCSS-specific options.
56
+
57
+ ### 3. `next.config.ts`
58
+
59
+ Wrap your Next.js config with `withMochi`:
60
+
61
+ ```typescript
62
+ // next.config.ts
63
+ import type { NextConfig } from "next"
64
+ import { withMochi } from "@mochi-css/next"
65
+
66
+ const nextConfig: NextConfig = {}
67
+
68
+ export default withMochi(nextConfig)
69
+ ```
70
+
71
+ ### 4. Import `globals.css` in your layout
72
+
73
+ Create a `src/app/globals.css` (or wherever your global stylesheet lives) and import it in your root layout:
74
+
75
+ ```tsx
76
+ // src/app/layout.tsx
77
+ import "./globals.css"
78
+
79
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
80
+ return (
81
+ <html lang="en">
82
+ <body>{children}</body>
83
+ </html>
84
+ )
85
+ }
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Turbopack
91
+
92
+ `withMochi` hooks into Turbopack automatically - but only if you have already opted in via your Next.js config.
93
+ Please explicitly specify in your Next.js config whether you are using turbopack to avoid configuration errors and unexpected behaviors.
94
+
95
+ **Next.js 15.3+ / 16** - use the top-level `turbopack` key:
96
+
97
+ ```typescript
98
+ // next.config.ts
99
+ const nextConfig: NextConfig = {
100
+ turbopack: {},
101
+ }
102
+
103
+ export default withMochi(nextConfig)
104
+ ```
105
+
106
+ **Next.js 14 / 15.0–15.2** - use `experimental.turbo`:
107
+
108
+ ```typescript
109
+ // next.config.ts
110
+ const nextConfig: NextConfig = {
111
+ experimental: {
112
+ turbo: {},
113
+ },
114
+ }
115
+
116
+ export default withMochi(nextConfig)
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Options
122
+
123
+ Most options are read automatically from `mochi.config.ts`.
124
+ See [`@mochi-css/config`](../config/README.md) for the full list.
125
+
126
+ The following option is specific to the Next.js integration:
127
+
128
+ | Option | Type | Default | Description |
129
+ |----------------|----------|----------------------------|----------------------------------------------------------------|
130
+ | `manifestPath` | `string` | `.mochi/manifest.json` | Path to the manifest written by PostCSS's `tmpDir` option |
131
+
132
+ ### `manifestPath`
133
+
134
+ Only set this if your PostCSS `tmpDir` is not the same as in the shared config:
135
+
136
+ ```typescript
137
+ export default withMochi(nextConfig, {
138
+ manifestPath: "custom-dir/manifest.json",
139
+ })
140
+ ```
141
+
142
+ The PostCSS `tmpDir` and the Next.js `manifestPath` must point to the same directory.
143
+ By default, they both use the value from the shared config, so no extra configuration is needed.
144
+
145
+ > Prefer setting options in `mochi.config.ts` - inline options override the file config but are not shared with other integrations.
package/dist/index.d.mts CHANGED
@@ -2,8 +2,23 @@ import { NextConfig } from "next";
2
2
 
3
3
  //#region src/index.d.ts
4
4
  type MochiNextOptions = {
5
+ /**
6
+ * Path to the `manifest.json` file produced by the PostCSS plugin's `tmpDir` option.
7
+ * The webpack/Turbopack loader reads this file to inject per-route CSS imports.
8
+ * Defaults to `.mochi/manifest.json` relative to the project root.
9
+ */
5
10
  manifestPath?: string;
6
11
  };
12
+ /**
13
+ * Wraps your Next.js config with Mochi CSS loaders.
14
+ *
15
+ * The loader automatically reads `mochi.config.ts` from the project root and applies
16
+ * any registered source transforms (e.g. `styledIdPlugin`) before Next.js compiles each file.
17
+ *
18
+ * Turbopack support requires you to explicitly opt in via your config:
19
+ * - Next.js 15.3+ / 16: add `turbopack: {}` to your next.config
20
+ * - Next.js 14 / 15.0–15.2: add `experimental: { turbo: {} }` to your next.config
21
+ */
7
22
  declare function withMochi(nextConfig: NextConfig, opts?: MochiNextOptions): NextConfig;
8
23
  //#endregion
9
24
  export { MochiNextOptions, withMochi };
package/dist/index.d.ts CHANGED
@@ -2,8 +2,23 @@ import { NextConfig } from "next";
2
2
 
3
3
  //#region src/index.d.ts
4
4
  type MochiNextOptions = {
5
+ /**
6
+ * Path to the `manifest.json` file produced by the PostCSS plugin's `tmpDir` option.
7
+ * The webpack/Turbopack loader reads this file to inject per-route CSS imports.
8
+ * Defaults to `.mochi/manifest.json` relative to the project root.
9
+ */
5
10
  manifestPath?: string;
6
11
  };
12
+ /**
13
+ * Wraps your Next.js config with Mochi CSS loaders.
14
+ *
15
+ * The loader automatically reads `mochi.config.ts` from the project root and applies
16
+ * any registered source transforms (e.g. `styledIdPlugin`) before Next.js compiles each file.
17
+ *
18
+ * Turbopack support requires you to explicitly opt in via your config:
19
+ * - Next.js 15.3+ / 16: add `turbopack: {}` to your next.config
20
+ * - Next.js 14 / 15.0–15.2: add `experimental: { turbo: {} }` to your next.config
21
+ */
7
22
  declare function withMochi(nextConfig: NextConfig, opts?: MochiNextOptions): NextConfig;
8
23
  //#endregion
9
24
  export { MochiNextOptions, withMochi };
package/dist/index.js CHANGED
@@ -1,36 +1,174 @@
1
1
  const require_chunk = require('./chunk-nOFOJqeH.js');
2
2
  let path = require("path");
3
3
  path = require_chunk.__toESM(path);
4
+ let fs = require("fs");
5
+ fs = require_chunk.__toESM(fs);
6
+ let __mochi_css_config = require("@mochi-css/config");
7
+ __mochi_css_config = require_chunk.__toESM(__mochi_css_config);
8
+ let __mochi_css_builder = require("@mochi-css/builder");
9
+ __mochi_css_builder = require_chunk.__toESM(__mochi_css_builder);
4
10
 
11
+ //#region src/watcher.ts
12
+ async function writeIfChanged(filePath, content) {
13
+ try {
14
+ if (await fs.default.promises.readFile(filePath, "utf-8") === content) return;
15
+ } catch {}
16
+ await fs.default.promises.writeFile(filePath, content, "utf-8");
17
+ }
18
+ async function writeCssFiles(css, tmpDir) {
19
+ await fs.default.promises.mkdir(tmpDir, { recursive: true });
20
+ const existingCssFiles = new Set((await fs.default.promises.readdir(tmpDir)).filter((f) => f.endsWith(".css") && f !== "global.css").map((f) => path.default.resolve(tmpDir, f)));
21
+ const diskManifest = {
22
+ files: {},
23
+ sourcemods: css.sourcemods
24
+ };
25
+ const writtenCssPaths = /* @__PURE__ */ new Set();
26
+ if (css.global) {
27
+ const globalPath = path.default.resolve(tmpDir, "global.css");
28
+ await writeIfChanged(globalPath, css.global);
29
+ diskManifest.global = globalPath;
30
+ }
31
+ for (const [source, fileCss] of Object.entries(css.files ?? {})) {
32
+ const hash = (0, __mochi_css_builder.fileHash)(source);
33
+ const cssPath = path.default.resolve(tmpDir, `${hash}.css`);
34
+ await writeIfChanged(cssPath, fileCss);
35
+ diskManifest.files[source] = cssPath;
36
+ writtenCssPaths.add(cssPath);
37
+ }
38
+ for (const existingPath of existingCssFiles) if (!writtenCssPaths.has(existingPath)) await fs.default.promises.unlink(existingPath);
39
+ await writeIfChanged(path.default.resolve(tmpDir, "manifest.json"), JSON.stringify(diskManifest));
40
+ }
41
+ let watcherStarted = false;
42
+ /**
43
+ * Sets up a file watcher that rebuilds Mochi CSS whenever source files change.
44
+ *
45
+ * Intended for development HMR. Called from `withMochi` — runs once per process.
46
+ */
47
+ async function startCssWatcher(tmpDir) {
48
+ if (watcherStarted) return;
49
+ watcherStarted = true;
50
+ const cwd = process.cwd();
51
+ const resolved = await (0, __mochi_css_config.resolveConfig)(await (0, __mochi_css_config.loadConfig)(), void 0, { extractors: __mochi_css_builder.defaultExtractors });
52
+ const effectiveTmpDir = resolved.tmpDir ? path.default.resolve(cwd, resolved.tmpDir) : tmpDir;
53
+ const debug = resolved.debug ?? false;
54
+ const absoluteRoots = resolved.roots.map((root) => typeof root === "string" ? path.default.resolve(cwd, root) : {
55
+ ...root,
56
+ path: path.default.resolve(cwd, root.path)
57
+ });
58
+ if (debug) console.log(`[mochi-css] watcher: roots=${JSON.stringify(absoluteRoots)}, tmpDir=${effectiveTmpDir}`);
59
+ if (absoluteRoots.length === 0) {
60
+ console.warn("[mochi-css] watcher: no roots configured — add `roots` to mochi.config.ts");
61
+ return;
62
+ }
63
+ const context = new __mochi_css_config.FullContext();
64
+ for (const plugin of resolved.plugins) plugin.onLoad?.(context);
65
+ const builder = new __mochi_css_builder.Builder({
66
+ roots: absoluteRoots,
67
+ extractors: resolved.extractors,
68
+ bundler: new __mochi_css_builder.RolldownBundler(),
69
+ runner: new __mochi_css_builder.VmRunner(),
70
+ splitCss: resolved.splitCss,
71
+ filePreProcess: ({ content, filePath }) => context.sourceTransform.transform(content, { filePath }),
72
+ astPostProcessors: context.getAnalysisHooks()
73
+ });
74
+ let rebuildTimer;
75
+ let rebuildGuard = Promise.resolve();
76
+ const scheduleRebuild = () => {
77
+ clearTimeout(rebuildTimer);
78
+ rebuildTimer = setTimeout(() => {
79
+ rebuildGuard = rebuildGuard.catch(() => {}).then(async () => {
80
+ if (debug) console.log("[mochi-css] watcher: rebuilding CSS…");
81
+ await writeCssFiles(await builder.collectMochiCss(), effectiveTmpDir);
82
+ if (debug) console.log("[mochi-css] watcher: CSS updated");
83
+ });
84
+ }, 50);
85
+ };
86
+ scheduleRebuild();
87
+ const rootDirs = absoluteRoots.map((root) => typeof root === "string" ? root : root.path);
88
+ for (const dir of rootDirs) {
89
+ if (!fs.default.existsSync(dir)) {
90
+ console.warn(`[mochi-css] watcher: root not found: ${dir}`);
91
+ continue;
92
+ }
93
+ if (debug) console.log(`[mochi-css] watcher: watching ${dir}`);
94
+ fs.default.watch(dir, { recursive: true }, (_, filename) => {
95
+ if (!filename || /\.(ts|tsx|js|jsx)$/.test(filename)) scheduleRebuild();
96
+ });
97
+ }
98
+ }
99
+
100
+ //#endregion
5
101
  //#region src/index.ts
6
102
  const MOCHI_DIR = ".mochi";
7
103
  const MANIFEST_FILE = "manifest.json";
104
+ /**
105
+ * Wraps your Next.js config with Mochi CSS loaders.
106
+ *
107
+ * The loader automatically reads `mochi.config.ts` from the project root and applies
108
+ * any registered source transforms (e.g. `styledIdPlugin`) before Next.js compiles each file.
109
+ *
110
+ * Turbopack support requires you to explicitly opt in via your config:
111
+ * - Next.js 15.3+ / 16: add `turbopack: {}` to your next.config
112
+ * - Next.js 14 / 15.0–15.2: add `experimental: { turbo: {} }` to your next.config
113
+ */
8
114
  function withMochi(nextConfig, opts) {
9
115
  const manifestPath = opts?.manifestPath ?? path.default.resolve(MOCHI_DIR, MANIFEST_FILE);
116
+ if (process.env["NODE_ENV"] !== "production") startCssWatcher(path.default.dirname(manifestPath)).catch((err) => {
117
+ console.error("[mochi-css] watcher error:", err instanceof Error ? err.message : err);
118
+ });
10
119
  const loaderPath = require.resolve("@mochi-css/next/loader");
11
120
  const loaderRule = {
12
121
  test: /\.(ts|tsx|js|jsx)$/,
13
122
  use: [{
14
123
  loader: loaderPath,
15
- options: { manifestPath }
124
+ options: {
125
+ manifestPath,
126
+ cwd: process.cwd()
127
+ }
16
128
  }]
17
129
  };
18
- const turbopack = nextConfig["turbopack"] ?? {};
19
- const turboRules = turbopack["rules"] ?? {};
20
- turboRules["*.{ts,tsx,js,jsx}"] = { loaders: [{
130
+ const turbopackRule = { loaders: [{
21
131
  loader: loaderPath,
22
- options: { manifestPath }
132
+ options: {
133
+ manifestPath,
134
+ cwd: process.cwd()
135
+ }
23
136
  }] };
24
- turbopack["rules"] = turboRules;
137
+ const existingTurbopack = nextConfig["turbopack"];
138
+ const turbopackPatch = existingTurbopack ? (() => {
139
+ const rules = existingTurbopack["rules"] ?? {};
140
+ rules["*.{ts,tsx,js,jsx}"] = turbopackRule;
141
+ return {
142
+ ...existingTurbopack,
143
+ rules
144
+ };
145
+ })() : void 0;
146
+ const existingExperimental = nextConfig["experimental"];
147
+ const existingExpTurbo = existingExperimental?.["turbo"];
148
+ const experimentalPatch = existingExpTurbo ? (() => {
149
+ const rules = existingExpTurbo["rules"] ?? {};
150
+ rules["*.{ts,tsx,js,jsx}"] = turbopackRule;
151
+ return {
152
+ ...existingExperimental,
153
+ turbo: {
154
+ ...existingExpTurbo,
155
+ rules
156
+ }
157
+ };
158
+ })() : void 0;
25
159
  const existingWebpack = nextConfig["webpack"];
26
160
  return {
27
161
  ...nextConfig,
28
- turbopack,
162
+ ...turbopackPatch !== void 0 && { turbopack: turbopackPatch },
163
+ ...experimentalPatch !== void 0 && { experimental: experimentalPatch },
29
164
  webpack(config, context) {
30
- const moduleConfig = config["module"];
31
- if (moduleConfig?.rules) moduleConfig.rules.push(loaderRule);
32
- if (existingWebpack) return existingWebpack(config, context);
33
- return config;
165
+ const result = existingWebpack ? existingWebpack(config, context) : config;
166
+ const mod = result["module"];
167
+ if (mod) {
168
+ mod.rules ??= [];
169
+ mod.rules.push(loaderRule);
170
+ } else result["module"] = { rules: [loaderRule] };
171
+ return result;
34
172
  }
35
173
  };
36
174
  }
package/dist/index.mjs CHANGED
@@ -1,39 +1,174 @@
1
1
  import { createRequire } from "node:module";
2
2
  import path from "path";
3
+ import fs from "fs";
4
+ import { FullContext, loadConfig, resolveConfig } from "@mochi-css/config";
5
+ import { Builder, RolldownBundler, VmRunner, defaultExtractors, fileHash } from "@mochi-css/builder";
3
6
 
4
7
  //#region rolldown:runtime
5
8
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
6
9
 
10
+ //#endregion
11
+ //#region src/watcher.ts
12
+ async function writeIfChanged(filePath, content) {
13
+ try {
14
+ if (await fs.promises.readFile(filePath, "utf-8") === content) return;
15
+ } catch {}
16
+ await fs.promises.writeFile(filePath, content, "utf-8");
17
+ }
18
+ async function writeCssFiles(css, tmpDir) {
19
+ await fs.promises.mkdir(tmpDir, { recursive: true });
20
+ const existingCssFiles = new Set((await fs.promises.readdir(tmpDir)).filter((f) => f.endsWith(".css") && f !== "global.css").map((f) => path.resolve(tmpDir, f)));
21
+ const diskManifest = {
22
+ files: {},
23
+ sourcemods: css.sourcemods
24
+ };
25
+ const writtenCssPaths = /* @__PURE__ */ new Set();
26
+ if (css.global) {
27
+ const globalPath = path.resolve(tmpDir, "global.css");
28
+ await writeIfChanged(globalPath, css.global);
29
+ diskManifest.global = globalPath;
30
+ }
31
+ for (const [source, fileCss] of Object.entries(css.files ?? {})) {
32
+ const hash = fileHash(source);
33
+ const cssPath = path.resolve(tmpDir, `${hash}.css`);
34
+ await writeIfChanged(cssPath, fileCss);
35
+ diskManifest.files[source] = cssPath;
36
+ writtenCssPaths.add(cssPath);
37
+ }
38
+ for (const existingPath of existingCssFiles) if (!writtenCssPaths.has(existingPath)) await fs.promises.unlink(existingPath);
39
+ await writeIfChanged(path.resolve(tmpDir, "manifest.json"), JSON.stringify(diskManifest));
40
+ }
41
+ let watcherStarted = false;
42
+ /**
43
+ * Sets up a file watcher that rebuilds Mochi CSS whenever source files change.
44
+ *
45
+ * Intended for development HMR. Called from `withMochi` — runs once per process.
46
+ */
47
+ async function startCssWatcher(tmpDir) {
48
+ if (watcherStarted) return;
49
+ watcherStarted = true;
50
+ const cwd = process.cwd();
51
+ const resolved = await resolveConfig(await loadConfig(), void 0, { extractors: defaultExtractors });
52
+ const effectiveTmpDir = resolved.tmpDir ? path.resolve(cwd, resolved.tmpDir) : tmpDir;
53
+ const debug = resolved.debug ?? false;
54
+ const absoluteRoots = resolved.roots.map((root) => typeof root === "string" ? path.resolve(cwd, root) : {
55
+ ...root,
56
+ path: path.resolve(cwd, root.path)
57
+ });
58
+ if (debug) console.log(`[mochi-css] watcher: roots=${JSON.stringify(absoluteRoots)}, tmpDir=${effectiveTmpDir}`);
59
+ if (absoluteRoots.length === 0) {
60
+ console.warn("[mochi-css] watcher: no roots configured — add `roots` to mochi.config.ts");
61
+ return;
62
+ }
63
+ const context = new FullContext();
64
+ for (const plugin of resolved.plugins) plugin.onLoad?.(context);
65
+ const builder = new Builder({
66
+ roots: absoluteRoots,
67
+ extractors: resolved.extractors,
68
+ bundler: new RolldownBundler(),
69
+ runner: new VmRunner(),
70
+ splitCss: resolved.splitCss,
71
+ filePreProcess: ({ content, filePath }) => context.sourceTransform.transform(content, { filePath }),
72
+ astPostProcessors: context.getAnalysisHooks()
73
+ });
74
+ let rebuildTimer;
75
+ let rebuildGuard = Promise.resolve();
76
+ const scheduleRebuild = () => {
77
+ clearTimeout(rebuildTimer);
78
+ rebuildTimer = setTimeout(() => {
79
+ rebuildGuard = rebuildGuard.catch(() => {}).then(async () => {
80
+ if (debug) console.log("[mochi-css] watcher: rebuilding CSS…");
81
+ await writeCssFiles(await builder.collectMochiCss(), effectiveTmpDir);
82
+ if (debug) console.log("[mochi-css] watcher: CSS updated");
83
+ });
84
+ }, 50);
85
+ };
86
+ scheduleRebuild();
87
+ const rootDirs = absoluteRoots.map((root) => typeof root === "string" ? root : root.path);
88
+ for (const dir of rootDirs) {
89
+ if (!fs.existsSync(dir)) {
90
+ console.warn(`[mochi-css] watcher: root not found: ${dir}`);
91
+ continue;
92
+ }
93
+ if (debug) console.log(`[mochi-css] watcher: watching ${dir}`);
94
+ fs.watch(dir, { recursive: true }, (_, filename) => {
95
+ if (!filename || /\.(ts|tsx|js|jsx)$/.test(filename)) scheduleRebuild();
96
+ });
97
+ }
98
+ }
99
+
7
100
  //#endregion
8
101
  //#region src/index.ts
9
102
  const MOCHI_DIR = ".mochi";
10
103
  const MANIFEST_FILE = "manifest.json";
104
+ /**
105
+ * Wraps your Next.js config with Mochi CSS loaders.
106
+ *
107
+ * The loader automatically reads `mochi.config.ts` from the project root and applies
108
+ * any registered source transforms (e.g. `styledIdPlugin`) before Next.js compiles each file.
109
+ *
110
+ * Turbopack support requires you to explicitly opt in via your config:
111
+ * - Next.js 15.3+ / 16: add `turbopack: {}` to your next.config
112
+ * - Next.js 14 / 15.0–15.2: add `experimental: { turbo: {} }` to your next.config
113
+ */
11
114
  function withMochi(nextConfig, opts) {
12
115
  const manifestPath = opts?.manifestPath ?? path.resolve(MOCHI_DIR, MANIFEST_FILE);
116
+ if (process.env["NODE_ENV"] !== "production") startCssWatcher(path.dirname(manifestPath)).catch((err) => {
117
+ console.error("[mochi-css] watcher error:", err instanceof Error ? err.message : err);
118
+ });
13
119
  const loaderPath = __require.resolve("@mochi-css/next/loader");
14
120
  const loaderRule = {
15
121
  test: /\.(ts|tsx|js|jsx)$/,
16
122
  use: [{
17
123
  loader: loaderPath,
18
- options: { manifestPath }
124
+ options: {
125
+ manifestPath,
126
+ cwd: process.cwd()
127
+ }
19
128
  }]
20
129
  };
21
- const turbopack = nextConfig["turbopack"] ?? {};
22
- const turboRules = turbopack["rules"] ?? {};
23
- turboRules["*.{ts,tsx,js,jsx}"] = { loaders: [{
130
+ const turbopackRule = { loaders: [{
24
131
  loader: loaderPath,
25
- options: { manifestPath }
132
+ options: {
133
+ manifestPath,
134
+ cwd: process.cwd()
135
+ }
26
136
  }] };
27
- turbopack["rules"] = turboRules;
137
+ const existingTurbopack = nextConfig["turbopack"];
138
+ const turbopackPatch = existingTurbopack ? (() => {
139
+ const rules = existingTurbopack["rules"] ?? {};
140
+ rules["*.{ts,tsx,js,jsx}"] = turbopackRule;
141
+ return {
142
+ ...existingTurbopack,
143
+ rules
144
+ };
145
+ })() : void 0;
146
+ const existingExperimental = nextConfig["experimental"];
147
+ const existingExpTurbo = existingExperimental?.["turbo"];
148
+ const experimentalPatch = existingExpTurbo ? (() => {
149
+ const rules = existingExpTurbo["rules"] ?? {};
150
+ rules["*.{ts,tsx,js,jsx}"] = turbopackRule;
151
+ return {
152
+ ...existingExperimental,
153
+ turbo: {
154
+ ...existingExpTurbo,
155
+ rules
156
+ }
157
+ };
158
+ })() : void 0;
28
159
  const existingWebpack = nextConfig["webpack"];
29
160
  return {
30
161
  ...nextConfig,
31
- turbopack,
162
+ ...turbopackPatch !== void 0 && { turbopack: turbopackPatch },
163
+ ...experimentalPatch !== void 0 && { experimental: experimentalPatch },
32
164
  webpack(config, context) {
33
- const moduleConfig = config["module"];
34
- if (moduleConfig?.rules) moduleConfig.rules.push(loaderRule);
35
- if (existingWebpack) return existingWebpack(config, context);
36
- return config;
165
+ const result = existingWebpack ? existingWebpack(config, context) : config;
166
+ const mod = result["module"];
167
+ if (mod) {
168
+ mod.rules ??= [];
169
+ mod.rules.push(loaderRule);
170
+ } else result["module"] = { rules: [loaderRule] };
171
+ return result;
37
172
  }
38
173
  };
39
174
  }
package/dist/loader.d.mts CHANGED
@@ -5,8 +5,13 @@ type LoaderContext = {
5
5
  manifestPath: string;
6
6
  };
7
7
  addDependency(path: string): void;
8
- callback(err: Error | null, content?: string, sourceMap?: unknown): void;
8
+ async(): (err: Error | null, content?: string, sourceMap?: unknown) => void;
9
9
  };
10
+ /**
11
+ * Webpack/Turbopack loader that:
12
+ * 1. Applies source transforms from the manifest (sourcemods produced by the PostCSS/builder pipeline).
13
+ * 2. Injects CSS `import` statements for per-file styles produced by the PostCSS plugin.
14
+ */
10
15
  declare function mochiLoader(this: LoaderContext, source: string): void;
11
16
  //#endregion
12
17
  export { mochiLoader as default };
package/dist/loader.d.ts CHANGED
@@ -5,7 +5,12 @@ type LoaderContext = {
5
5
  manifestPath: string;
6
6
  };
7
7
  addDependency(path: string): void;
8
- callback(err: Error | null, content?: string, sourceMap?: unknown): void;
8
+ async(): (err: Error | null, content?: string, sourceMap?: unknown) => void;
9
9
  };
10
+ /**
11
+ * Webpack/Turbopack loader that:
12
+ * 1. Applies source transforms from the manifest (sourcemods produced by the PostCSS/builder pipeline).
13
+ * 2. Injects CSS `import` statements for per-file styles produced by the PostCSS plugin.
14
+ */
10
15
  declare function mochiLoader(this: LoaderContext, source: string): void;
11
16
  export = mochiLoader;
package/dist/loader.js CHANGED
@@ -3,32 +3,65 @@ let path = require("path");
3
3
  path = require_chunk.__toESM(path);
4
4
  let fs = require("fs");
5
5
  fs = require_chunk.__toESM(fs);
6
+ let diff = require("diff");
7
+ diff = require_chunk.__toESM(diff);
6
8
 
7
9
  //#region src/loader.ts
10
+ let manifestCache;
11
+ function readManifest(manifestPath) {
12
+ const stat = fs.default.statSync(manifestPath, { throwIfNoEntry: false });
13
+ if (!stat) return null;
14
+ if (manifestCache?.path === manifestPath && manifestCache.mtime === stat.mtimeMs) return manifestCache.manifest;
15
+ const manifest = JSON.parse(fs.default.readFileSync(manifestPath, "utf-8"));
16
+ manifestCache = {
17
+ path: manifestPath,
18
+ mtime: stat.mtimeMs,
19
+ manifest
20
+ };
21
+ return manifest;
22
+ }
8
23
  function injectImports(ctx, manifest, source) {
9
24
  const cssPath = manifest.files[ctx.resourcePath];
10
25
  if (!cssPath) return source;
11
26
  const imports = [];
27
+ const sourceDir = path.default.dirname(ctx.resourcePath);
28
+ function toImportPath(absPath) {
29
+ let rel = path.default.relative(sourceDir, absPath).replaceAll("\\", "/");
30
+ if (!rel.startsWith(".")) rel = "./" + rel;
31
+ return rel;
32
+ }
12
33
  const absoluteCssPath = path.default.resolve(cssPath);
13
- imports.push(`import ${JSON.stringify(absoluteCssPath)};`);
34
+ imports.push(`import ${JSON.stringify(toImportPath(absoluteCssPath))};`);
14
35
  ctx.addDependency(absoluteCssPath);
15
36
  if (manifest.global) {
16
37
  const absoluteGlobalPath = path.default.resolve(manifest.global);
17
- imports.push(`import ${JSON.stringify(absoluteGlobalPath)};`);
38
+ imports.push(`import ${JSON.stringify(toImportPath(absoluteGlobalPath))};`);
18
39
  ctx.addDependency(absoluteGlobalPath);
19
40
  }
20
41
  return imports.join("\n") + "\n" + source;
21
42
  }
43
+ /**
44
+ * Webpack/Turbopack loader that:
45
+ * 1. Applies source transforms from the manifest (sourcemods produced by the PostCSS/builder pipeline).
46
+ * 2. Injects CSS `import` statements for per-file styles produced by the PostCSS plugin.
47
+ */
22
48
  function mochiLoader(source) {
23
49
  const { manifestPath } = this.getOptions();
50
+ const callback = this.async();
51
+ const resourcePath = this.resourcePath;
24
52
  this.addDependency(manifestPath);
25
- if (!fs.default.existsSync(manifestPath)) {
26
- this.callback(null, source);
27
- return;
53
+ try {
54
+ const manifest = readManifest(manifestPath);
55
+ if (!manifest) {
56
+ callback(null, source);
57
+ return;
58
+ }
59
+ const sourcemod = manifest.sourcemods?.[resourcePath];
60
+ const transformed = sourcemod ? (0, diff.applyPatch)(source, sourcemod) || source : source;
61
+ callback(null, injectImports(this, manifest, transformed));
62
+ } catch (err) {
63
+ callback(err instanceof Error ? err : new Error(String(err)));
28
64
  }
29
- const content = fs.default.readFileSync(manifestPath, "utf-8");
30
- const manifest = JSON.parse(content);
31
- this.callback(null, injectImports(this, manifest, source));
32
65
  }
33
66
 
34
67
  //#endregion
package/dist/loader.mjs CHANGED
@@ -1,31 +1,63 @@
1
1
  import path from "path";
2
2
  import fs from "fs";
3
+ import { applyPatch } from "diff";
3
4
 
4
5
  //#region src/loader.ts
6
+ let manifestCache;
7
+ function readManifest(manifestPath) {
8
+ const stat = fs.statSync(manifestPath, { throwIfNoEntry: false });
9
+ if (!stat) return null;
10
+ if (manifestCache?.path === manifestPath && manifestCache.mtime === stat.mtimeMs) return manifestCache.manifest;
11
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
12
+ manifestCache = {
13
+ path: manifestPath,
14
+ mtime: stat.mtimeMs,
15
+ manifest
16
+ };
17
+ return manifest;
18
+ }
5
19
  function injectImports(ctx, manifest, source) {
6
20
  const cssPath = manifest.files[ctx.resourcePath];
7
21
  if (!cssPath) return source;
8
22
  const imports = [];
23
+ const sourceDir = path.dirname(ctx.resourcePath);
24
+ function toImportPath(absPath) {
25
+ let rel = path.relative(sourceDir, absPath).replaceAll("\\", "/");
26
+ if (!rel.startsWith(".")) rel = "./" + rel;
27
+ return rel;
28
+ }
9
29
  const absoluteCssPath = path.resolve(cssPath);
10
- imports.push(`import ${JSON.stringify(absoluteCssPath)};`);
30
+ imports.push(`import ${JSON.stringify(toImportPath(absoluteCssPath))};`);
11
31
  ctx.addDependency(absoluteCssPath);
12
32
  if (manifest.global) {
13
33
  const absoluteGlobalPath = path.resolve(manifest.global);
14
- imports.push(`import ${JSON.stringify(absoluteGlobalPath)};`);
34
+ imports.push(`import ${JSON.stringify(toImportPath(absoluteGlobalPath))};`);
15
35
  ctx.addDependency(absoluteGlobalPath);
16
36
  }
17
37
  return imports.join("\n") + "\n" + source;
18
38
  }
39
+ /**
40
+ * Webpack/Turbopack loader that:
41
+ * 1. Applies source transforms from the manifest (sourcemods produced by the PostCSS/builder pipeline).
42
+ * 2. Injects CSS `import` statements for per-file styles produced by the PostCSS plugin.
43
+ */
19
44
  function mochiLoader(source) {
20
45
  const { manifestPath } = this.getOptions();
46
+ const callback = this.async();
47
+ const resourcePath = this.resourcePath;
21
48
  this.addDependency(manifestPath);
22
- if (!fs.existsSync(manifestPath)) {
23
- this.callback(null, source);
24
- return;
49
+ try {
50
+ const manifest = readManifest(manifestPath);
51
+ if (!manifest) {
52
+ callback(null, source);
53
+ return;
54
+ }
55
+ const sourcemod = manifest.sourcemods?.[resourcePath];
56
+ const transformed = sourcemod ? applyPatch(source, sourcemod) || source : source;
57
+ callback(null, injectImports(this, manifest, transformed));
58
+ } catch (err) {
59
+ callback(err instanceof Error ? err : new Error(String(err)));
25
60
  }
26
- const content = fs.readFileSync(manifestPath, "utf-8");
27
- const manifest = JSON.parse(content);
28
- this.callback(null, injectImports(this, manifest, source));
29
61
  }
30
62
 
31
63
  //#endregion
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mochi-css/next",
3
3
  "repository": "git@github.com:Niikelion/mochi-css.git",
4
- "version": "2.0.0",
4
+ "version": "3.0.0",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -32,16 +32,22 @@
32
32
  }
33
33
  },
34
34
  "scripts": {
35
- "build": "tsc --noEmit && tsdown"
35
+ "build": "tsc --noEmit && tsdown",
36
+ "test": "vitest",
37
+ "coverage": "vitest run --coverage"
38
+ },
39
+ "dependencies": {
40
+ "@mochi-css/builder": "^3.0.0",
41
+ "@mochi-css/config": "^3.0.0",
42
+ "diff": "^8.0.3",
43
+ "next": "^14 || ^15 || ^16"
36
44
  },
37
45
  "devDependencies": {
38
- "@arethetypeswrong/cli": "^0.18.2",
39
- "@gamedev-sensei/ts-config": "^2.1.0",
40
- "@gamedev-sensei/tsdown-config": "^2.1.0",
46
+ "@mochi-css/shared-config": "^2.0.0",
47
+ "@mochi-css/test": "^2.0.0",
48
+ "@types/diff": "^8.0.0",
41
49
  "@types/node": "^24.8.1",
42
- "tsdown": "^0.15.7"
43
- },
44
- "peerDependencies": {
45
- "next": "^14 || ^15 || ^16"
50
+ "typescript": "^5.9.3",
51
+ "vitest": "^4.0.15"
46
52
  }
47
53
  }