@mochi-css/next 2.0.1 → 3.0.1
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 +145 -0
- package/dist/index.d.mts +15 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +153 -11
- package/dist/index.mjs +150 -11
- package/dist/loader.d.mts +6 -1
- package/dist/loader.d.ts +6 -1
- package/dist/loader.js +41 -8
- package/dist/loader.mjs +40 -8
- package/package.json +19 -8
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,178 @@
|
|
|
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, {});
|
|
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(resolved.onDiagnostic ?? (() => {}));
|
|
64
|
+
for (const plugin of resolved.plugins) plugin.onLoad?.(context);
|
|
65
|
+
const builder = new __mochi_css_builder.Builder({
|
|
66
|
+
roots: absoluteRoots,
|
|
67
|
+
stages: [...context.stages.getAll()],
|
|
68
|
+
bundler: new __mochi_css_builder.RolldownBundler(),
|
|
69
|
+
runner: new __mochi_css_builder.VmRunner(),
|
|
70
|
+
splitCss: resolved.splitCss,
|
|
71
|
+
filePreProcess: ({ content, filePath }) => context.filePreProcess.transform(content, { filePath }),
|
|
72
|
+
sourceTransforms: [...context.sourceTransforms.getAll()],
|
|
73
|
+
emitHooks: [...context.emitHooks.getAll()],
|
|
74
|
+
cleanup: () => {
|
|
75
|
+
context.cleanup.runAll();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
let rebuildTimer;
|
|
79
|
+
let rebuildGuard = Promise.resolve();
|
|
80
|
+
const scheduleRebuild = () => {
|
|
81
|
+
clearTimeout(rebuildTimer);
|
|
82
|
+
rebuildTimer = setTimeout(() => {
|
|
83
|
+
rebuildGuard = rebuildGuard.catch(() => {}).then(async () => {
|
|
84
|
+
if (debug) console.log("[mochi-css] watcher: rebuilding CSS…");
|
|
85
|
+
await writeCssFiles(await builder.collectMochiCss(), effectiveTmpDir);
|
|
86
|
+
if (debug) console.log("[mochi-css] watcher: CSS updated");
|
|
87
|
+
});
|
|
88
|
+
}, 50);
|
|
89
|
+
};
|
|
90
|
+
scheduleRebuild();
|
|
91
|
+
const rootDirs = absoluteRoots.map((root) => typeof root === "string" ? root : root.path);
|
|
92
|
+
for (const dir of rootDirs) {
|
|
93
|
+
if (!fs.default.existsSync(dir)) {
|
|
94
|
+
console.warn(`[mochi-css] watcher: root not found: ${dir}`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (debug) console.log(`[mochi-css] watcher: watching ${dir}`);
|
|
98
|
+
fs.default.watch(dir, { recursive: true }, (_, filename) => {
|
|
99
|
+
if (!filename || /\.(ts|tsx|js|jsx)$/.test(filename)) scheduleRebuild();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
//#endregion
|
|
5
105
|
//#region src/index.ts
|
|
6
106
|
const MOCHI_DIR = ".mochi";
|
|
7
107
|
const MANIFEST_FILE = "manifest.json";
|
|
108
|
+
/**
|
|
109
|
+
* Wraps your Next.js config with Mochi CSS loaders.
|
|
110
|
+
*
|
|
111
|
+
* The loader automatically reads `mochi.config.ts` from the project root and applies
|
|
112
|
+
* any registered source transforms (e.g. `styledIdPlugin`) before Next.js compiles each file.
|
|
113
|
+
*
|
|
114
|
+
* Turbopack support requires you to explicitly opt in via your config:
|
|
115
|
+
* - Next.js 15.3+ / 16: add `turbopack: {}` to your next.config
|
|
116
|
+
* - Next.js 14 / 15.0–15.2: add `experimental: { turbo: {} }` to your next.config
|
|
117
|
+
*/
|
|
8
118
|
function withMochi(nextConfig, opts) {
|
|
9
119
|
const manifestPath = opts?.manifestPath ?? path.default.resolve(MOCHI_DIR, MANIFEST_FILE);
|
|
120
|
+
if (process.env["NODE_ENV"] !== "production") startCssWatcher(path.default.dirname(manifestPath)).catch((err) => {
|
|
121
|
+
console.error("[mochi-css] watcher error:", err instanceof Error ? err.message : err);
|
|
122
|
+
});
|
|
10
123
|
const loaderPath = require.resolve("@mochi-css/next/loader");
|
|
11
124
|
const loaderRule = {
|
|
12
125
|
test: /\.(ts|tsx|js|jsx)$/,
|
|
13
126
|
use: [{
|
|
14
127
|
loader: loaderPath,
|
|
15
|
-
options: {
|
|
128
|
+
options: {
|
|
129
|
+
manifestPath,
|
|
130
|
+
cwd: process.cwd()
|
|
131
|
+
}
|
|
16
132
|
}]
|
|
17
133
|
};
|
|
18
|
-
const
|
|
19
|
-
const turboRules = turbopack["rules"] ?? {};
|
|
20
|
-
turboRules["*.{ts,tsx,js,jsx}"] = { loaders: [{
|
|
134
|
+
const turbopackRule = { loaders: [{
|
|
21
135
|
loader: loaderPath,
|
|
22
|
-
options: {
|
|
136
|
+
options: {
|
|
137
|
+
manifestPath,
|
|
138
|
+
cwd: process.cwd()
|
|
139
|
+
}
|
|
23
140
|
}] };
|
|
24
|
-
|
|
141
|
+
const existingTurbopack = nextConfig["turbopack"];
|
|
142
|
+
const turbopackPatch = existingTurbopack ? (() => {
|
|
143
|
+
const rules = existingTurbopack["rules"] ?? {};
|
|
144
|
+
rules["*.{ts,tsx,js,jsx}"] = turbopackRule;
|
|
145
|
+
return {
|
|
146
|
+
...existingTurbopack,
|
|
147
|
+
rules
|
|
148
|
+
};
|
|
149
|
+
})() : void 0;
|
|
150
|
+
const existingExperimental = nextConfig["experimental"];
|
|
151
|
+
const existingExpTurbo = existingExperimental?.["turbo"];
|
|
152
|
+
const experimentalPatch = existingExpTurbo ? (() => {
|
|
153
|
+
const rules = existingExpTurbo["rules"] ?? {};
|
|
154
|
+
rules["*.{ts,tsx,js,jsx}"] = turbopackRule;
|
|
155
|
+
return {
|
|
156
|
+
...existingExperimental,
|
|
157
|
+
turbo: {
|
|
158
|
+
...existingExpTurbo,
|
|
159
|
+
rules
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
})() : void 0;
|
|
25
163
|
const existingWebpack = nextConfig["webpack"];
|
|
26
164
|
return {
|
|
27
165
|
...nextConfig,
|
|
28
|
-
turbopack,
|
|
166
|
+
...turbopackPatch !== void 0 && { turbopack: turbopackPatch },
|
|
167
|
+
...experimentalPatch !== void 0 && { experimental: experimentalPatch },
|
|
29
168
|
webpack(config, context) {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
if (
|
|
33
|
-
|
|
169
|
+
const result = existingWebpack ? existingWebpack(config, context) : config;
|
|
170
|
+
const mod = result["module"];
|
|
171
|
+
if (mod) {
|
|
172
|
+
mod.rules ??= [];
|
|
173
|
+
mod.rules.push(loaderRule);
|
|
174
|
+
} else result["module"] = { rules: [loaderRule] };
|
|
175
|
+
return result;
|
|
34
176
|
}
|
|
35
177
|
};
|
|
36
178
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -1,39 +1,178 @@
|
|
|
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, 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, {});
|
|
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(resolved.onDiagnostic ?? (() => {}));
|
|
64
|
+
for (const plugin of resolved.plugins) plugin.onLoad?.(context);
|
|
65
|
+
const builder = new Builder({
|
|
66
|
+
roots: absoluteRoots,
|
|
67
|
+
stages: [...context.stages.getAll()],
|
|
68
|
+
bundler: new RolldownBundler(),
|
|
69
|
+
runner: new VmRunner(),
|
|
70
|
+
splitCss: resolved.splitCss,
|
|
71
|
+
filePreProcess: ({ content, filePath }) => context.filePreProcess.transform(content, { filePath }),
|
|
72
|
+
sourceTransforms: [...context.sourceTransforms.getAll()],
|
|
73
|
+
emitHooks: [...context.emitHooks.getAll()],
|
|
74
|
+
cleanup: () => {
|
|
75
|
+
context.cleanup.runAll();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
let rebuildTimer;
|
|
79
|
+
let rebuildGuard = Promise.resolve();
|
|
80
|
+
const scheduleRebuild = () => {
|
|
81
|
+
clearTimeout(rebuildTimer);
|
|
82
|
+
rebuildTimer = setTimeout(() => {
|
|
83
|
+
rebuildGuard = rebuildGuard.catch(() => {}).then(async () => {
|
|
84
|
+
if (debug) console.log("[mochi-css] watcher: rebuilding CSS…");
|
|
85
|
+
await writeCssFiles(await builder.collectMochiCss(), effectiveTmpDir);
|
|
86
|
+
if (debug) console.log("[mochi-css] watcher: CSS updated");
|
|
87
|
+
});
|
|
88
|
+
}, 50);
|
|
89
|
+
};
|
|
90
|
+
scheduleRebuild();
|
|
91
|
+
const rootDirs = absoluteRoots.map((root) => typeof root === "string" ? root : root.path);
|
|
92
|
+
for (const dir of rootDirs) {
|
|
93
|
+
if (!fs.existsSync(dir)) {
|
|
94
|
+
console.warn(`[mochi-css] watcher: root not found: ${dir}`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (debug) console.log(`[mochi-css] watcher: watching ${dir}`);
|
|
98
|
+
fs.watch(dir, { recursive: true }, (_, filename) => {
|
|
99
|
+
if (!filename || /\.(ts|tsx|js|jsx)$/.test(filename)) scheduleRebuild();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
7
104
|
//#endregion
|
|
8
105
|
//#region src/index.ts
|
|
9
106
|
const MOCHI_DIR = ".mochi";
|
|
10
107
|
const MANIFEST_FILE = "manifest.json";
|
|
108
|
+
/**
|
|
109
|
+
* Wraps your Next.js config with Mochi CSS loaders.
|
|
110
|
+
*
|
|
111
|
+
* The loader automatically reads `mochi.config.ts` from the project root and applies
|
|
112
|
+
* any registered source transforms (e.g. `styledIdPlugin`) before Next.js compiles each file.
|
|
113
|
+
*
|
|
114
|
+
* Turbopack support requires you to explicitly opt in via your config:
|
|
115
|
+
* - Next.js 15.3+ / 16: add `turbopack: {}` to your next.config
|
|
116
|
+
* - Next.js 14 / 15.0–15.2: add `experimental: { turbo: {} }` to your next.config
|
|
117
|
+
*/
|
|
11
118
|
function withMochi(nextConfig, opts) {
|
|
12
119
|
const manifestPath = opts?.manifestPath ?? path.resolve(MOCHI_DIR, MANIFEST_FILE);
|
|
120
|
+
if (process.env["NODE_ENV"] !== "production") startCssWatcher(path.dirname(manifestPath)).catch((err) => {
|
|
121
|
+
console.error("[mochi-css] watcher error:", err instanceof Error ? err.message : err);
|
|
122
|
+
});
|
|
13
123
|
const loaderPath = __require.resolve("@mochi-css/next/loader");
|
|
14
124
|
const loaderRule = {
|
|
15
125
|
test: /\.(ts|tsx|js|jsx)$/,
|
|
16
126
|
use: [{
|
|
17
127
|
loader: loaderPath,
|
|
18
|
-
options: {
|
|
128
|
+
options: {
|
|
129
|
+
manifestPath,
|
|
130
|
+
cwd: process.cwd()
|
|
131
|
+
}
|
|
19
132
|
}]
|
|
20
133
|
};
|
|
21
|
-
const
|
|
22
|
-
const turboRules = turbopack["rules"] ?? {};
|
|
23
|
-
turboRules["*.{ts,tsx,js,jsx}"] = { loaders: [{
|
|
134
|
+
const turbopackRule = { loaders: [{
|
|
24
135
|
loader: loaderPath,
|
|
25
|
-
options: {
|
|
136
|
+
options: {
|
|
137
|
+
manifestPath,
|
|
138
|
+
cwd: process.cwd()
|
|
139
|
+
}
|
|
26
140
|
}] };
|
|
27
|
-
|
|
141
|
+
const existingTurbopack = nextConfig["turbopack"];
|
|
142
|
+
const turbopackPatch = existingTurbopack ? (() => {
|
|
143
|
+
const rules = existingTurbopack["rules"] ?? {};
|
|
144
|
+
rules["*.{ts,tsx,js,jsx}"] = turbopackRule;
|
|
145
|
+
return {
|
|
146
|
+
...existingTurbopack,
|
|
147
|
+
rules
|
|
148
|
+
};
|
|
149
|
+
})() : void 0;
|
|
150
|
+
const existingExperimental = nextConfig["experimental"];
|
|
151
|
+
const existingExpTurbo = existingExperimental?.["turbo"];
|
|
152
|
+
const experimentalPatch = existingExpTurbo ? (() => {
|
|
153
|
+
const rules = existingExpTurbo["rules"] ?? {};
|
|
154
|
+
rules["*.{ts,tsx,js,jsx}"] = turbopackRule;
|
|
155
|
+
return {
|
|
156
|
+
...existingExperimental,
|
|
157
|
+
turbo: {
|
|
158
|
+
...existingExpTurbo,
|
|
159
|
+
rules
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
})() : void 0;
|
|
28
163
|
const existingWebpack = nextConfig["webpack"];
|
|
29
164
|
return {
|
|
30
165
|
...nextConfig,
|
|
31
|
-
turbopack,
|
|
166
|
+
...turbopackPatch !== void 0 && { turbopack: turbopackPatch },
|
|
167
|
+
...experimentalPatch !== void 0 && { experimental: experimentalPatch },
|
|
32
168
|
webpack(config, context) {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
if (
|
|
36
|
-
|
|
169
|
+
const result = existingWebpack ? existingWebpack(config, context) : config;
|
|
170
|
+
const mod = result["module"];
|
|
171
|
+
if (mod) {
|
|
172
|
+
mod.rules ??= [];
|
|
173
|
+
mod.rules.push(loaderRule);
|
|
174
|
+
} else result["module"] = { rules: [loaderRule] };
|
|
175
|
+
return result;
|
|
37
176
|
}
|
|
38
177
|
};
|
|
39
178
|
}
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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": "
|
|
4
|
+
"version": "3.0.1",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"module": "dist/index.mjs",
|
|
@@ -32,16 +32,27 @@
|
|
|
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"
|
|
36
38
|
},
|
|
37
|
-
"
|
|
38
|
-
"@
|
|
39
|
-
"@
|
|
40
|
-
"
|
|
41
|
-
"@types/node": "^24.8.1",
|
|
42
|
-
"tsdown": "^0.15.7"
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@mochi-css/builder": "^4.0.0",
|
|
41
|
+
"@mochi-css/config": "^3.1.0",
|
|
42
|
+
"diff": "^8.0.3"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"next": "^14 || ^15 || ^16"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@mochi-css/shared-config": "^2.0.0",
|
|
49
|
+
"@mochi-css/test": "^2.0.0",
|
|
50
|
+
"@types/diff": "^8.0.0",
|
|
51
|
+
"@types/node": "^24.8.1",
|
|
52
|
+
"next": "^16",
|
|
53
|
+
"react": "^19",
|
|
54
|
+
"react-dom": "^19",
|
|
55
|
+
"typescript": "^5.9.3",
|
|
56
|
+
"vitest": "^4.0.15"
|
|
46
57
|
}
|
|
47
58
|
}
|