@kasperpeulen/vitest-plugin-nextjs 0.1.0-alpha.0 → 0.1.0-alpha.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 +1 -1
- package/dist/compile.d.ts +10 -2
- package/dist/compile.js +47 -12
- package/dist/document.js +2 -0
- package/dist/host-return-shared.d.ts +5 -0
- package/dist/host-return-shared.js +5 -0
- package/dist/host-return.d.ts +5 -0
- package/dist/host-return.js +16 -0
- package/dist/not-found-page.d.ts +19 -0
- package/dist/not-found-page.js +74 -0
- package/dist/plugin.d.ts +2 -1
- package/dist/plugin.js +15 -4
- package/dist/project/webpack.d.ts +21 -2
- package/dist/project/webpack.js +40 -9
- package/dist/project.d.ts +10 -5
- package/dist/rsc.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ That also makes it a nice loop for coding agents: edit, run, fix, repeat.
|
|
|
55
55
|
|
|
56
56
|
### Is it really Next.js?
|
|
57
57
|
|
|
58
|
-
Yes. The plugin doesn't reimplement Next. It swaps the bundler for Vite and runs Next's runtime unchanged. To keep that honest, the repo runs Next's own e2e tests against the plugin: **
|
|
58
|
+
Yes. The plugin doesn't reimplement Next. It swaps the bundler for Vite and runs Next's runtime unchanged. To keep that honest, the repo runs Next's own e2e tests against the plugin: **967 of the 1,139 that apply pass (85%)** on `next@16.4.0`. Every failure is listed with a reason in the [conformance report](https://github.com/storybookjs/storybook-for-pages/blob/main/docs/internals/next-conformance.md). CI also tests against `next@latest` and `next@canary`.
|
|
59
59
|
|
|
60
60
|
## Quick start
|
|
61
61
|
|
package/dist/compile.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { Plugin } from "vite";
|
|
|
8
8
|
* module.
|
|
9
9
|
*/
|
|
10
10
|
declare function fontRequestOf(id: string): string | undefined;
|
|
11
|
+
declare const codeFile: RegExp;
|
|
11
12
|
declare function createDependencyCompilePlugin(getProject: () => NextProject, layer: NextLayer, isAppCode: (file: string, layer: NextLayer) => boolean): {
|
|
12
13
|
name: string;
|
|
13
14
|
resolveId(source: string): {
|
|
@@ -19,8 +20,15 @@ declare function createDependencyCompilePlugin(getProject: () => NextProject, la
|
|
|
19
20
|
map: string | null;
|
|
20
21
|
} | undefined>;
|
|
21
22
|
};
|
|
22
|
-
declare function createScanCompilePlugin(getProject: () => NextProject, layer: NextLayer, isAppCode: (file: string, layer: NextLayer) => boolean): {
|
|
23
|
+
declare function createScanCompilePlugin(getProject: () => NextProject, layer: NextLayer, isAppCode: (file: string, layer: NextLayer) => boolean, resolve: (source: string, importer: string) => Promise<string | undefined>): {
|
|
23
24
|
name: string;
|
|
25
|
+
resolveId(source: string, importer: string | undefined): Promise<{
|
|
26
|
+
id: string;
|
|
27
|
+
} | undefined>;
|
|
28
|
+
load(id: string): Promise<{
|
|
29
|
+
code: string;
|
|
30
|
+
moduleType: "jsx";
|
|
31
|
+
} | undefined>;
|
|
24
32
|
transform(code: string, id: string): Promise<{
|
|
25
33
|
code: string;
|
|
26
34
|
map: string | null;
|
|
@@ -28,4 +36,4 @@ declare function createScanCompilePlugin(getProject: () => NextProject, layer: N
|
|
|
28
36
|
};
|
|
29
37
|
declare function createCompilePlugin(getProject: () => NextProject, layerOf: (environment: string) => NextLayer | undefined, isAppCode: (file: string, layer: NextLayer) => boolean): Plugin;
|
|
30
38
|
//#endregion
|
|
31
|
-
export { createCompilePlugin, createDependencyCompilePlugin, createScanCompilePlugin, fontRequestOf };
|
|
39
|
+
export { codeFile, createCompilePlugin, createDependencyCompilePlugin, createScanCompilePlugin, fontRequestOf };
|
package/dist/compile.js
CHANGED
|
@@ -18,6 +18,7 @@ function fontRequestOf(id) {
|
|
|
18
18
|
return Buffer.from(name.slice(0, -4), "base64url").toString();
|
|
19
19
|
}
|
|
20
20
|
const sourceFile = /\.(?:tsx|ts|jsx|js|mjs)$/;
|
|
21
|
+
const codeFile = /\.(?:[cm]?[jt]sx?)$/;
|
|
21
22
|
function createDependencyCompilePlugin(getProject, layer, isAppCode) {
|
|
22
23
|
return {
|
|
23
24
|
name: "vitest-plugin-rsc:next-compile-dependency",
|
|
@@ -38,9 +39,33 @@ function createDependencyCompilePlugin(getProject, layer, isAppCode) {
|
|
|
38
39
|
}
|
|
39
40
|
};
|
|
40
41
|
}
|
|
41
|
-
function createScanCompilePlugin(getProject, layer, isAppCode) {
|
|
42
|
+
function createScanCompilePlugin(getProject, layer, isAppCode, resolve) {
|
|
43
|
+
const ruleOf = (id) => {
|
|
44
|
+
const file = id.split("?")[0];
|
|
45
|
+
if (codeFile.test(file) || file.endsWith(".json")) return;
|
|
46
|
+
return getProject().webpackRuleOf(id, layer);
|
|
47
|
+
};
|
|
42
48
|
return {
|
|
43
49
|
name: `vitest-plugin-rsc:next-compile-scan:${layer}`,
|
|
50
|
+
async resolveId(source, importer) {
|
|
51
|
+
if (!importer) return;
|
|
52
|
+
const named = source.startsWith(".") ? path.join(path.dirname(importer), source) : source;
|
|
53
|
+
if (!ruleOf(named)) return;
|
|
54
|
+
const id = await resolve(source, importer);
|
|
55
|
+
if (!id || !ruleOf(id) || !isAppCode(id.split("?")[0], layer)) return;
|
|
56
|
+
return { id };
|
|
57
|
+
},
|
|
58
|
+
async load(id) {
|
|
59
|
+
const rule = ruleOf(id);
|
|
60
|
+
const file = id.split("?")[0];
|
|
61
|
+
if (!rule || !path.isAbsolute(file) || !fs.existsSync(file)) return;
|
|
62
|
+
const code = await getProject().loadWithWebpackRule(id, layer).catch(() => "");
|
|
63
|
+
const compiled = rule.compile && code ? await getProject().compile(code, file, layer).catch(() => void 0) : void 0;
|
|
64
|
+
return {
|
|
65
|
+
code: compiled && !compiled.refused ? compiled.code : code ?? "",
|
|
66
|
+
moduleType: "jsx"
|
|
67
|
+
};
|
|
68
|
+
},
|
|
44
69
|
async transform(code, id) {
|
|
45
70
|
if (id.includes("?") || !sourceFile.test(id) || !isAppCode(id, layer)) return;
|
|
46
71
|
const compiled = await getProject().compile(code, id, layer).catch(() => void 0);
|
|
@@ -114,27 +139,37 @@ function createCompilePlugin(getProject, layerOf, isAppCode) {
|
|
|
114
139
|
return `import ${JSON.stringify(stylesheet)};\nexport default ${JSON.stringify(exports)};\n`;
|
|
115
140
|
}
|
|
116
141
|
const layer = layerOf(this.environment.name);
|
|
117
|
-
if (!layer
|
|
118
|
-
|
|
142
|
+
if (!layer) return;
|
|
143
|
+
const file = id.split("?")[0];
|
|
144
|
+
if (!codeFile.test(file) && getProject().webpackRuleOf(id, layer) && fs.existsSync(file)) return {
|
|
119
145
|
code: await getProject().loadWithWebpackRule(id, layer),
|
|
120
|
-
moduleType: "js"
|
|
146
|
+
moduleType: file.endsWith(".json") ? "json" : "js"
|
|
121
147
|
};
|
|
122
|
-
if (!getProject().isImage(id) || !fs.existsSync(id)) return;
|
|
148
|
+
if (id.includes("?") || !getProject().isImage(id) || !fs.existsSync(id)) return;
|
|
123
149
|
return built(await getProject().loadImage(id), "js");
|
|
124
150
|
},
|
|
125
151
|
async transform(code, id) {
|
|
126
152
|
const layer = layerOf(this.environment.name);
|
|
127
|
-
if (!layer
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
153
|
+
if (!layer) return;
|
|
154
|
+
const rule = getProject().webpackRuleOf(id, layer);
|
|
155
|
+
const file = id.split("?")[0];
|
|
156
|
+
const compiles = rule?.compile || !id.includes("?") && sourceFile.test(id);
|
|
157
|
+
const loads = rule && codeFile.test(file);
|
|
158
|
+
if (!compiles && !loads) return;
|
|
159
|
+
if (!isAppCode(file, layer) || !fs.existsSync(file)) return;
|
|
160
|
+
if (file.startsWith(`${normalizePath(this.environment.config.cacheDir)}/`)) return;
|
|
161
|
+
const loaded = loads ? await getProject().loadWithWebpackRule(id, layer, code) : code;
|
|
162
|
+
const compiled = compiles ? await getProject().compile(loaded, file, layer) : void 0;
|
|
163
|
+
if (compiled) return {
|
|
133
164
|
code: compiled.code,
|
|
134
165
|
map: compiled.map ?? null
|
|
135
166
|
};
|
|
167
|
+
if (loaded !== code) return {
|
|
168
|
+
code: loaded,
|
|
169
|
+
map: null
|
|
170
|
+
};
|
|
136
171
|
}
|
|
137
172
|
};
|
|
138
173
|
}
|
|
139
174
|
//#endregion
|
|
140
|
-
export { createCompilePlugin, createDependencyCompilePlugin, createScanCompilePlugin, fontRequestOf };
|
|
175
|
+
export { codeFile, createCompilePlugin, createDependencyCompilePlugin, createScanCompilePlugin, fontRequestOf };
|
package/dist/document.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import "./styles-shared.js";
|
|
2
|
+
import { rememberHost } from "./host-return.js";
|
|
2
3
|
import pageHost from "virtual:vitest-plugin-rsc/page-host";
|
|
3
4
|
import { releaseTimers, runnerTimers } from "@kasperpeulen/vitest-plugin-rsc/internal/timers";
|
|
4
5
|
//#region src/document.ts
|
|
@@ -169,6 +170,7 @@ async function loadDocument(html, url, container, after, inKeptApp = false) {
|
|
|
169
170
|
preloads = [];
|
|
170
171
|
const runnerUrl = window.location.href;
|
|
171
172
|
runnerHref = runnerUrl;
|
|
173
|
+
rememberHost(runnerUrl);
|
|
172
174
|
const runnerBody = document.body;
|
|
173
175
|
const runnerAttributes = elements(document).map(attributesOf);
|
|
174
176
|
const reactTargets = [
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { buildDirectory } from "./built-files.js";
|
|
2
|
+
import { hostKey, topLevelHostKey } from "./host-return-shared.js";
|
|
3
|
+
//#region src/host-return.ts
|
|
4
|
+
const ownStorages = ["sessionStorage", "localStorage"].map((name) => Object.getOwnPropertyDescriptor(window, name)?.get);
|
|
5
|
+
/** Keeps `url`, the URL of the host's document, as the one a page that opens now goes back to. */
|
|
6
|
+
function rememberHost(url) {
|
|
7
|
+
if (!buildDirectory) return;
|
|
8
|
+
const [session, local] = ownStorages.map((get) => get?.call(window));
|
|
9
|
+
try {
|
|
10
|
+
session?.setItem(hostKey, url);
|
|
11
|
+
session?.setItem(topLevelHostKey, window.top === window ? "1" : "0");
|
|
12
|
+
local?.setItem(hostKey, url);
|
|
13
|
+
} catch {}
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { rememberHost };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Plugin } from "vite";
|
|
2
|
+
|
|
3
|
+
//#region src/not-found-page.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* A script that loads the host's document of the last page of a static build
|
|
6
|
+
* that this browser opened, when the expression `when` holds: in place of a
|
|
7
|
+
* document that the static host has at the URL of that page. Nothing when no
|
|
8
|
+
* page was opened, or the host is at this URL itself.
|
|
9
|
+
*/
|
|
10
|
+
declare function returnToHostScript(when?: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* An expression of a script for `returnToHostScript()`: whether the host's
|
|
13
|
+
* document of the last page that this tab opened was the tab's own, not in
|
|
14
|
+
* a frame.
|
|
15
|
+
*/
|
|
16
|
+
declare const topLevelHost: string;
|
|
17
|
+
declare function notFoundPagePlugin(environment: string): Plugin;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { notFoundPagePlugin, returnToHostScript, topLevelHost };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { hostKey, topLevelHostKey } from "./host-return-shared.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { getPluginApi } from "@vitejs/plugin-rsc/plugin";
|
|
5
|
+
//#region src/not-found-page.ts
|
|
6
|
+
/**
|
|
7
|
+
* A script that loads the host's document of the last page of a static build
|
|
8
|
+
* that this browser opened, when the expression `when` holds: in place of a
|
|
9
|
+
* document that the static host has at the URL of that page. Nothing when no
|
|
10
|
+
* page was opened, or the host is at this URL itself.
|
|
11
|
+
*/
|
|
12
|
+
function returnToHostScript(when = "true") {
|
|
13
|
+
const key = JSON.stringify(hostKey);
|
|
14
|
+
return `(function () {
|
|
15
|
+
if (!(${when})) return;
|
|
16
|
+
var host;
|
|
17
|
+
try {
|
|
18
|
+
host = sessionStorage.getItem(${key}) || localStorage.getItem(${key});
|
|
19
|
+
} catch (error) {}
|
|
20
|
+
if (!host) return;
|
|
21
|
+
var url = new URL(host, location.href);
|
|
22
|
+
if (url.origin !== location.origin || url.pathname === location.pathname) return;
|
|
23
|
+
// What the document started to load is not needed.
|
|
24
|
+
window.stop();
|
|
25
|
+
location.replace(url.href);
|
|
26
|
+
})();`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* An expression of a script for `returnToHostScript()`: whether the host's
|
|
30
|
+
* document of the last page that this tab opened was the tab's own, not in
|
|
31
|
+
* a frame.
|
|
32
|
+
*/
|
|
33
|
+
const topLevelHost = `(function () {
|
|
34
|
+
try {
|
|
35
|
+
return sessionStorage.getItem(${JSON.stringify(topLevelHostKey)}) === "1";
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
})()`;
|
|
40
|
+
const notFoundPage = `<!doctype html>
|
|
41
|
+
<html lang="en">
|
|
42
|
+
<head>
|
|
43
|
+
<meta charset="utf-8" />
|
|
44
|
+
<meta name="robots" content="noindex" />
|
|
45
|
+
<title>404: This page could not be found.</title>
|
|
46
|
+
<script>
|
|
47
|
+
${returnToHostScript()}
|
|
48
|
+
<\/script>
|
|
49
|
+
</head>
|
|
50
|
+
<body>
|
|
51
|
+
<p>404: This page could not be found.</p>
|
|
52
|
+
</body>
|
|
53
|
+
</html>
|
|
54
|
+
`;
|
|
55
|
+
function notFoundPagePlugin(environment) {
|
|
56
|
+
return {
|
|
57
|
+
name: "vitest-plugin-rsc:next-not-found-page",
|
|
58
|
+
apply: "build",
|
|
59
|
+
generateBundle(_options, bundle) {
|
|
60
|
+
if (this.environment.name !== environment) return;
|
|
61
|
+
if (getPluginApi(this.environment.config)?.manager.isScanBuild) return;
|
|
62
|
+
const { publicDir } = this.environment.config;
|
|
63
|
+
if ("404.html" in bundle) return;
|
|
64
|
+
if (publicDir && fs.existsSync(path.join(publicDir, "404.html"))) return;
|
|
65
|
+
this.emitFile({
|
|
66
|
+
type: "asset",
|
|
67
|
+
fileName: "404.html",
|
|
68
|
+
source: notFoundPage
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
export { notFoundPagePlugin, returnToHostScript, topLevelHost };
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Mode, ModeOptions, SpeedOptions } from "./mode.js";
|
|
2
2
|
import { NextBuild } from "./build.js";
|
|
3
|
+
import { returnToHostScript, topLevelHost } from "./not-found-page.js";
|
|
3
4
|
import { ServerCodeOptions } from "./server-code.js";
|
|
4
5
|
import { Plugin } from "vite";
|
|
5
6
|
//#region src/plugin.d.ts
|
|
@@ -19,4 +20,4 @@ type VitestPluginNextOptions = ServerCodeOptions & ModeOptions & {
|
|
|
19
20
|
};
|
|
20
21
|
declare function vitestPluginNext(options?: VitestPluginNextOptions): Plugin[];
|
|
21
22
|
//#endregion
|
|
22
|
-
export { type Mode, type NextBuild, type SpeedOptions, VitestPluginNextOptions, vitestPluginNext };
|
|
23
|
+
export { type Mode, type NextBuild, type SpeedOptions, VitestPluginNextOptions, returnToHostScript, topLevelHost, vitestPluginNext };
|
package/dist/plugin.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { registryGlobal } from "./registry-global.js";
|
|
2
2
|
import { nextBuilds, nextProductionPlugin } from "./build.js";
|
|
3
|
-
import { createCompilePlugin, createDependencyCompilePlugin, createScanCompilePlugin } from "./compile.js";
|
|
3
|
+
import { codeFile, createCompilePlugin, createDependencyCompilePlugin, createScanCompilePlugin } from "./compile.js";
|
|
4
4
|
import { builtHostFileOf, changesInPlace, createStyles } from "./styles.js";
|
|
5
5
|
import { clientFiles, clientReferenceOfModule } from "./client-files.js";
|
|
6
6
|
import { createCommonJsPlugin } from "./commonjs.js";
|
|
@@ -11,6 +11,7 @@ import { resolveSpeedFeatures } from "./mode.js";
|
|
|
11
11
|
import { browserMapping, isNativeNodeModule, isNodeModule, nativeRequirePlugin, nodeModuleOf } from "./node-builtins.js";
|
|
12
12
|
import { devAssetPrefix, prerenderImports, runtimeImports as runtimeImports$1, serverReferenceInfo } from "./compat/paths.js";
|
|
13
13
|
import { createNodePlatform, instrumentationPlugin } from "./node-platform.js";
|
|
14
|
+
import { notFoundPagePlugin, returnToHostScript, topLevelHost } from "./not-found-page.js";
|
|
14
15
|
import { optimizerCacheKey } from "./optimizer-cache-key.js";
|
|
15
16
|
import "./own-package.js";
|
|
16
17
|
import { pageTimersDefine, pageTimersPlugin } from "./page-timers.js";
|
|
@@ -46,7 +47,14 @@ const environmentOf = {
|
|
|
46
47
|
const layers = Object.keys(environmentOf);
|
|
47
48
|
const registry = `globalThis.${registryGlobal}`;
|
|
48
49
|
const fileEntry = (file) => escapePath(normalizePath(file));
|
|
49
|
-
const
|
|
50
|
+
const ruleExtensionsOf = (project) => (project.config.pageExtensions ?? []).filter((extension) => !codeFile.test(`.${extension}`) && project.webpackRuleOf(path.join(project.appDir, `page.${extension}`), "rsc"));
|
|
51
|
+
const appEntriesOf = (project) => `${fileEntry(project.appDir)}/**/*.{${[
|
|
52
|
+
"js",
|
|
53
|
+
"jsx",
|
|
54
|
+
"ts",
|
|
55
|
+
"tsx",
|
|
56
|
+
...ruleExtensionsOf(project)
|
|
57
|
+
].join(",")}}`;
|
|
50
58
|
const manifestId = "virtual:vitest-plugin-rsc/next-manifest";
|
|
51
59
|
const virtual = (name) => `virtual:vitest-plugin-rsc/next-${name}`;
|
|
52
60
|
const routeEntryPrefix = virtual("route/");
|
|
@@ -640,12 +648,14 @@ function vitestPluginNext(options = {}) {
|
|
|
640
648
|
])
|
|
641
649
|
};
|
|
642
650
|
const dependenciesOfNext = (layer) => Object.entries(project.aliases[layer]).flatMap(([key, target]) => key.endsWith("$") && target && resolvers[layer].isDependencyOfNext(target) ? [`next > ${key.slice(0, -1)}`] : []);
|
|
651
|
+
const ruleExtensions = ruleExtensionsOf(project).map((extension) => `.${extension}`);
|
|
643
652
|
const optimizeDeps = (layer) => ({
|
|
644
653
|
include: [
|
|
645
654
|
...include[layer],
|
|
646
655
|
...dependenciesOfNext(layer),
|
|
647
656
|
...project.webpackPackages[layer]
|
|
648
657
|
],
|
|
658
|
+
...ruleExtensions.length > 0 && { extensions: ruleExtensions },
|
|
649
659
|
rolldownOptions: {
|
|
650
660
|
plugins: [
|
|
651
661
|
...build === "production" ? [nextProductionPlugin(project.nextDir)] : [],
|
|
@@ -653,7 +663,7 @@ function vitestPluginNext(options = {}) {
|
|
|
653
663
|
...layer === "browser" ? [hostPackagesPlugin] : [],
|
|
654
664
|
resolvers[layer].plugin(),
|
|
655
665
|
createDependencyCompilePlugin(getProject, layer, serverCode.isAppCode),
|
|
656
|
-
createScanCompilePlugin(getProject, layer, serverCode.isAppCode),
|
|
666
|
+
createScanCompilePlugin(getProject, layer, serverCode.isAppCode, async (source, importer) => (await server?.environments[environmentOf[layer]]?.pluginContainer.resolveId(source, importer))?.id),
|
|
657
667
|
...layer === "rsc" ? [nextClientBoundaryPlugin(getProject, resolvers.rsc)] : [],
|
|
658
668
|
flightConsolePlugin(),
|
|
659
669
|
nextPatchesPlugin(),
|
|
@@ -921,6 +931,7 @@ function vitestPluginNext(options = {}) {
|
|
|
921
931
|
[environmentOf.ssr]: "ssr"
|
|
922
932
|
}),
|
|
923
933
|
pageTimersPlugin(environmentOf.browser, serverCode.isHostFile),
|
|
934
|
+
notFoundPagePlugin(environmentOf.rsc),
|
|
924
935
|
...styles.plugins,
|
|
925
936
|
routesAheadPlugin({
|
|
926
937
|
root: () => getProject().root,
|
|
@@ -992,4 +1003,4 @@ function dependencyBuildPlugin(getProject, serverCode, layer) {
|
|
|
992
1003
|
};
|
|
993
1004
|
}
|
|
994
1005
|
//#endregion
|
|
995
|
-
export { vitestPluginNext };
|
|
1006
|
+
export { returnToHostScript, topLevelHost, vitestPluginNext };
|
|
@@ -3,15 +3,26 @@ import { NextContext } from "./context.js";
|
|
|
3
3
|
import { NextProject } from "../project.js";
|
|
4
4
|
|
|
5
5
|
//#region src/project/webpack.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* What a rule matches, as webpack takes it: a string is the start of the path,
|
|
8
|
+
* a function decides, and a list matches where one of it does.
|
|
9
|
+
*/
|
|
10
|
+
type WebpackCondition = RegExp | string | ((value: string) => boolean) | WebpackCondition[];
|
|
6
11
|
/** A rule of the `webpack` function: the files it is for, and their loaders. */
|
|
7
12
|
type WebpackRule = {
|
|
8
|
-
|
|
13
|
+
/** Of the path of the file, as `include` and `exclude`. */test: WebpackCondition;
|
|
14
|
+
include?: WebpackCondition;
|
|
15
|
+
exclude?: WebpackCondition; /** Of the query of the file, `?collection=docs`, with its `?`. */
|
|
16
|
+
resourceQuery?: WebpackCondition;
|
|
17
|
+
enforce?: "pre" | "post"; /** As webpack calls them: the last first. */
|
|
9
18
|
loaders: {
|
|
10
19
|
loader: string;
|
|
11
20
|
options: unknown;
|
|
12
21
|
}[]; /** Whether Next's compiler, `defaultLoaders.babel`, runs after them. */
|
|
13
22
|
compile: boolean;
|
|
14
23
|
};
|
|
24
|
+
/** The loaders of the rules that match a file, as webpack runs them. */
|
|
25
|
+
type WebpackLoaders = Pick<WebpackRule, "loaders" | "compile">;
|
|
15
26
|
type WebpackFunction = {
|
|
16
27
|
rules: WebpackRule[]; /** The candidates of each alias it sets, in order: the first that resolves is the one. */
|
|
17
28
|
aliases: Record<string, string[]>; /** What of it does not apply, to say when a run starts. */
|
|
@@ -28,6 +39,14 @@ declare function readWebpackFunction(webpack: unknown, options: {
|
|
|
28
39
|
buildId: string;
|
|
29
40
|
config: unknown;
|
|
30
41
|
}): WebpackFunction;
|
|
42
|
+
/**
|
|
43
|
+
* The loaders of the rules for a file, as webpack matches them: `test`,
|
|
44
|
+
* `include` and `exclude` its path, `resourceQuery` its query. Each rule that
|
|
45
|
+
* matches adds its loaders, as webpack runs them: those of `enforce: "pre"`
|
|
46
|
+
* first, then the others, then those of `enforce: "post"`. Fumadocs matches an
|
|
47
|
+
* `.mdx` file with `?collection=docs`, and a `meta.json` with it by its query.
|
|
48
|
+
*/
|
|
49
|
+
declare function loadersOf(rules: WebpackRule[], id: string): WebpackLoaders | undefined;
|
|
31
50
|
/**
|
|
32
51
|
* The candidates of an alias for an import, as webpack takes them: a name
|
|
33
52
|
* matches the import and what is under it, and one that ends with `$` only
|
|
@@ -43,4 +62,4 @@ declare function importOfFile(file: string): string;
|
|
|
43
62
|
/** The rules and aliases of the `webpack` function in `next.config`, for each layer. */
|
|
44
63
|
declare function createWebpackRules(context: NextContext, runLoader: RunLoader): Pick<NextProject, "webpackRuleOf" | "loadWithWebpackRule" | "webpackAliasOf" | "webpackPackages" | "webpackDifferences">;
|
|
45
64
|
//#endregion
|
|
46
|
-
export { WebpackFunction, WebpackRule, aliasOf, createWebpackRules, importOfFile, readWebpackFunction };
|
|
65
|
+
export { WebpackCondition, WebpackFunction, WebpackLoaders, WebpackRule, aliasOf, createWebpackRules, importOfFile, loadersOf, readWebpackFunction };
|
package/dist/project/webpack.js
CHANGED
|
@@ -39,7 +39,7 @@ function readWebpackFunction(webpack, options) {
|
|
|
39
39
|
return read;
|
|
40
40
|
}
|
|
41
41
|
for (const rule of after?.module?.rules ?? []) {
|
|
42
|
-
const { test, use, ...rest } = rule ?? {};
|
|
42
|
+
const { test, include, exclude, resourceQuery, enforce, use, ...rest } = rule ?? {};
|
|
43
43
|
const uses = Array.isArray(use) ? use : [use];
|
|
44
44
|
const loaders = uses.flatMap((entry) => {
|
|
45
45
|
if (entry === babel) return [];
|
|
@@ -53,12 +53,20 @@ function readWebpackFunction(webpack, options) {
|
|
|
53
53
|
options
|
|
54
54
|
}] : [];
|
|
55
55
|
});
|
|
56
|
-
if (!(test
|
|
57
|
-
|
|
56
|
+
if (!isCondition(test) || [
|
|
57
|
+
include,
|
|
58
|
+
exclude,
|
|
59
|
+
resourceQuery
|
|
60
|
+
].some((condition) => condition !== void 0 && !isCondition(condition)) || enforce !== void 0 && enforce !== "pre" && enforce !== "post" || Object.keys(rest).length > 0 || loaders.length + (uses.includes(babel) ? 1 : 0) !== uses.length) {
|
|
61
|
+
read.differences.push(`a rule of the \`webpack\` function of next.config does not apply: only one with a \`test\` and the loaders in \`use\` does, with \`include\`, \`exclude\`, \`resourceQuery\` and \`enforce\` or not, not ${String(test ?? "this one")}`);
|
|
58
62
|
continue;
|
|
59
63
|
}
|
|
60
64
|
read.rules.push({
|
|
61
65
|
test,
|
|
66
|
+
...include !== void 0 && { include },
|
|
67
|
+
...exclude !== void 0 && { exclude },
|
|
68
|
+
...resourceQuery !== void 0 && { resourceQuery },
|
|
69
|
+
...enforce !== void 0 && { enforce },
|
|
62
70
|
loaders,
|
|
63
71
|
compile: uses.includes(babel)
|
|
64
72
|
});
|
|
@@ -70,6 +78,29 @@ function readWebpackFunction(webpack, options) {
|
|
|
70
78
|
if ((after?.plugins?.length ?? 0) > 0) read.differences.push("the plugins of webpack that the `webpack` function of next.config adds do not run");
|
|
71
79
|
return read;
|
|
72
80
|
}
|
|
81
|
+
const isCondition = (condition) => condition instanceof RegExp || typeof condition === "string" || typeof condition === "function" || Array.isArray(condition) && condition.every(isCondition);
|
|
82
|
+
const matches = (condition, value) => condition instanceof RegExp ? condition.test(value) : typeof condition === "string" ? value.startsWith(condition) : typeof condition === "function" ? condition(value) : condition.some((entry) => matches(entry, value));
|
|
83
|
+
const viteQuery = /[?&](?:raw|url|inline|no-inline|worker|worker_file|sharedworker)(?=[=&]|$)/;
|
|
84
|
+
/**
|
|
85
|
+
* The loaders of the rules for a file, as webpack matches them: `test`,
|
|
86
|
+
* `include` and `exclude` its path, `resourceQuery` its query. Each rule that
|
|
87
|
+
* matches adds its loaders, as webpack runs them: those of `enforce: "pre"`
|
|
88
|
+
* first, then the others, then those of `enforce: "post"`. Fumadocs matches an
|
|
89
|
+
* `.mdx` file with `?collection=docs`, and a `meta.json` with it by its query.
|
|
90
|
+
*/
|
|
91
|
+
function loadersOf(rules, id) {
|
|
92
|
+
if (viteQuery.test(id)) return;
|
|
93
|
+
const queryAt = id.indexOf("?");
|
|
94
|
+
const file = queryAt < 0 ? id : id.slice(0, queryAt);
|
|
95
|
+
const query = queryAt < 0 ? "" : id.slice(queryAt);
|
|
96
|
+
const matched = rules.filter((rule) => matches(rule.test, file) && (rule.include === void 0 || matches(rule.include, file)) && (rule.exclude === void 0 || !matches(rule.exclude, file)) && (rule.resourceQuery === void 0 || matches(rule.resourceQuery, query)));
|
|
97
|
+
if (matched.length === 0) return;
|
|
98
|
+
const phase = ({ enforce }) => enforce === "post" ? 0 : enforce === "pre" ? 2 : 1;
|
|
99
|
+
return {
|
|
100
|
+
loaders: matched.toSorted((a, b) => phase(a) - phase(b)).flatMap((rule) => rule.loaders),
|
|
101
|
+
compile: matched.some((rule) => rule.compile)
|
|
102
|
+
};
|
|
103
|
+
}
|
|
73
104
|
/**
|
|
74
105
|
* The candidates of an alias for an import, as webpack takes them: a name
|
|
75
106
|
* matches the import and what is under it, and one that ends with `$` only
|
|
@@ -114,7 +145,7 @@ function createWebpackRules(context, runLoader) {
|
|
|
114
145
|
ssr: server,
|
|
115
146
|
browser: client
|
|
116
147
|
};
|
|
117
|
-
const ruleOf = (
|
|
148
|
+
const ruleOf = (id, layer) => loadersOf(of[layer].rules, id);
|
|
118
149
|
const loaders = /* @__PURE__ */ new Map();
|
|
119
150
|
const loaderAt = (request) => {
|
|
120
151
|
let loader = loaders.get(request);
|
|
@@ -141,14 +172,14 @@ function createWebpackRules(context, runLoader) {
|
|
|
141
172
|
ssr: packagesOf(server),
|
|
142
173
|
browser: packagesOf(client)
|
|
143
174
|
},
|
|
144
|
-
async loadWithWebpackRule(
|
|
145
|
-
const rule = ruleOf(
|
|
175
|
+
async loadWithWebpackRule(id, layer, source) {
|
|
176
|
+
const rule = ruleOf(id, layer);
|
|
146
177
|
if (!rule) return;
|
|
147
|
-
let code = await fs.promises.readFile(
|
|
178
|
+
let code = source ?? await fs.promises.readFile(id.split("?")[0]);
|
|
148
179
|
for (const { loader, options } of rule.loaders.toReversed()) {
|
|
149
180
|
const run = loaderAt(loader);
|
|
150
181
|
const input = run.raw ? Buffer.from(code) : String(code);
|
|
151
|
-
[code] = await runLoader(run, options,
|
|
182
|
+
[code] = await runLoader(run, options, id, input);
|
|
152
183
|
}
|
|
153
184
|
return String(code);
|
|
154
185
|
},
|
|
@@ -157,4 +188,4 @@ function createWebpackRules(context, runLoader) {
|
|
|
157
188
|
};
|
|
158
189
|
}
|
|
159
190
|
//#endregion
|
|
160
|
-
export { aliasOf, createWebpackRules, importOfFile, readWebpackFunction };
|
|
191
|
+
export { aliasOf, createWebpackRules, importOfFile, loadersOf, readWebpackFunction };
|
package/dist/project.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { RoutesManifest } from "./compat/types.js";
|
|
|
3
3
|
import { FlightEntry } from "./flight.js";
|
|
4
4
|
import { PageEnv } from "./project/env.js";
|
|
5
5
|
import { NewExport } from "./node-platform.js";
|
|
6
|
-
import {
|
|
6
|
+
import { WebpackLoaders } from "./project/webpack.js";
|
|
7
7
|
import { ResolveRoutesParams } from "@next/routing";
|
|
8
8
|
import * as _$vite from "vite";
|
|
9
9
|
import { IncomingMessage, ServerResponse } from "node:http";
|
|
@@ -155,11 +155,16 @@ type NextProject = {
|
|
|
155
155
|
*/
|
|
156
156
|
dependencyCompileOptions: Record<NextLayer, unknown>;
|
|
157
157
|
/**
|
|
158
|
-
* The
|
|
159
|
-
*
|
|
158
|
+
* The loaders of the rules of the `webpack` function in `next.config` for a
|
|
159
|
+
* module, like the rule of `@next/mdx` for an `.mdx` file, or Fumadocs' for
|
|
160
|
+
* one with `?collection=docs`: see project/webpack.ts.
|
|
160
161
|
*/
|
|
161
|
-
webpackRuleOf(
|
|
162
|
-
|
|
162
|
+
webpackRuleOf(id: string, layer: NextLayer): WebpackLoaders | undefined;
|
|
163
|
+
/**
|
|
164
|
+
* What those loaders make of the file, or of `source`, its code as Vite
|
|
165
|
+
* read it. Nothing for a module without them.
|
|
166
|
+
*/
|
|
167
|
+
loadWithWebpackRule(id: string, layer: NextLayer, source?: string): Promise<string | undefined>; /** The candidates of an alias of the `webpack` function for an import, in order. */
|
|
163
168
|
webpackAliasOf(source: string, layer: NextLayer): string[] | undefined; /** The packages among those candidates, to pre-bundle. */
|
|
164
169
|
webpackPackages: Record<NextLayer, string[]>; /** What of the `webpack` function does not apply, to say when a run starts. */
|
|
165
170
|
webpackDifferences: string[];
|
package/dist/rsc.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { actionModulePrefix, registry } from "./registry.js";
|
|
2
2
|
import { builtStylesheetsOf, stylesheetsEvent } from "./styles-shared.js";
|
|
3
|
-
import { preloadPage } from "./document.js";
|
|
4
3
|
import { buildDirectory, builtFiles } from "./built-files.js";
|
|
4
|
+
import { preloadPage } from "./document.js";
|
|
5
5
|
import { NoFallbackError } from "./compat/rsc/handlers.js";
|
|
6
6
|
import { nextConfig, speedFeatures } from "virtual:vitest-plugin-rsc/next-manifest";
|
|
7
7
|
import { askDevServer, versionsOfAnswers } from "@kasperpeulen/vitest-plugin-rsc/internal/utils";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kasperpeulen/vitest-plugin-nextjs",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
4
|
"description": "Test and render Next.js App Router pages with Vite: in Vitest Browser Mode, vite dev and Storybook, with Next's own runtime.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nextjs",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"tinyglobby": "^0.2.17"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
|
-
"@next/routing": "16.4.
|
|
79
|
+
"@next/routing": "16.4.3",
|
|
80
80
|
"@noble/hashes": "2.4.0",
|
|
81
81
|
"@types/node": "^24.12.4",
|
|
82
82
|
"@types/react": "^19.2.14",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"fflate": "0.8.3",
|
|
87
87
|
"happy-dom": "^21.0.4",
|
|
88
88
|
"memfs": "4.80.0",
|
|
89
|
-
"next": "16.4.
|
|
89
|
+
"next": "16.4.3",
|
|
90
90
|
"node-inspect-extracted": "3.3.4",
|
|
91
91
|
"path-browserify": "1.0.1",
|
|
92
92
|
"react": "^19.2.6",
|