Skip to content

Commit b466fac

Browse files
fix: nitroV2Plugin compress public assets by default
fixes #2096 - match `1.x` behavior for easier migration; can still be overridden to false for users who prefer uncompressed files. - extract common build test utilities. - fix tree shaking tests that were using unreliable regex (didn't account for minification and variable renaming); replace with a unique string identifier for better resilience, and add a complementary test for the tree shaking with side effects case. - add vitest `~` alias configuration, as the solid plugin wasn't resolving it in the test environment.
1 parent 798b285 commit b466fac

File tree

8 files changed

+105
-54
lines changed

8 files changed

+105
-54
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { describe, expect, it } from "vitest";
2+
import { getBuildOutputDirs, getFiles } from "~/utils/build-output-utils";
3+
4+
describe("public assets compression", () => {
5+
it("includes at least one .gz and one .br file in client output", async () => {
6+
const { clientOutputRoot } = getBuildOutputDirs();
7+
const gzFiles = await getFiles(clientOutputRoot, /\.gz$/);
8+
const brFiles = await getFiles(clientOutputRoot, /\.br$/);
9+
10+
// Only files above 1KB are compressed, so we check that at least one .gz and one .br file exists
11+
expect(
12+
gzFiles.length,
13+
`No .gz files found in client output: ${clientOutputRoot}`,
14+
).toBeGreaterThan(0);
15+
expect(
16+
brFiles.length,
17+
`No .br files found in client output: ${clientOutputRoot}`,
18+
).toBeGreaterThan(0);
19+
});
20+
});

apps/tests/src/routes/treeshaking/(no-side-effects).tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createAsync } from "@solidjs/router";
22

3-
const a = 1;
3+
const a = "myTreeshakingTestUniqueString1";
44

55
function getA() {
66
return a;

apps/tests/src/routes/treeshaking/server-secret-leak.server.test.ts

Lines changed: 2 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,13 @@
1-
import { existsSync } from "node:fs";
2-
import { readdir, readFile } from "node:fs/promises";
3-
import path from "node:path";
4-
import { brotliDecompressSync, gunzipSync } from "node:zlib";
51
import { describe, expect, it } from "vitest";
2+
import { getBuildOutputDirs, getFiles, readFileContent } from "~/utils/build-output-utils";
63

74
// Avoid full pattern to exclude this file from scan
85
const SECRET_MARKER = new RegExp(`${"MyServer"}${"SuperSecretUniqueString"}\\d+`, "g");
96
const ALL_FILE_EXTENSIONS = /\.(ts|tsx|js|jsx|mjs|cjs|mts|cts|css|map|gz|br)$/;
107

118
describe("server code does not leak to client bundle", () => {
129
it("verifies secret markers are server-only and not in client output", async () => {
13-
const appRoot = process.cwd();
14-
const sourceRoot = path.join(appRoot, "src");
15-
const serverOutputRoot = path.join(appRoot, ".output/server");
16-
const clientOutputRoot = path.join(appRoot, ".output/public");
17-
18-
// Verify required directories exist
19-
expect(existsSync(sourceRoot), `Source dir not found: ${sourceRoot}`).toBe(true);
20-
expect(
21-
existsSync(serverOutputRoot),
22-
`Server output dir not found: ${serverOutputRoot}. Did you run the build? (pnpm --filter tests run build)`,
23-
).toBe(true);
24-
expect(
25-
existsSync(clientOutputRoot),
26-
`Client output dir not found: ${clientOutputRoot}. Did you run the build? (pnpm --filter tests run build)`,
27-
).toBe(true);
10+
const { sourceRoot, serverOutputRoot, clientOutputRoot } = getBuildOutputDirs();
2811

2912
// Collect and validate markers from source code
3013
const sourceMarkerCounts = await countSourceMarkers(sourceRoot);
@@ -77,22 +60,3 @@ async function countSourceMarkers(rootDir: string) {
7760
}
7861
return markerCounts;
7962
}
80-
81-
async function getFiles(dir: string, fileRegex: RegExp): Promise<string[]> {
82-
const entries = await readdir(dir, { recursive: true, withFileTypes: true });
83-
return entries
84-
.filter(e => e.isFile() && fileRegex.test(e.name))
85-
.map(e => path.join(e.parentPath, e.name));
86-
}
87-
88-
async function readFileContent(filePath: string) {
89-
if (filePath.endsWith(".br")) {
90-
return brotliDecompressSync(await readFile(filePath)).toString("utf-8");
91-
}
92-
93-
if (filePath.endsWith(".gz")) {
94-
return gunzipSync(await readFile(filePath)).toString("utf-8");
95-
}
96-
97-
return readFile(filePath, "utf-8");
98-
}

apps/tests/src/routes/treeshaking/side-effects.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createAsync } from "@solidjs/router";
22

3-
export const a = 1;
3+
export const a = "myTreeshakingTestUniqueString2";
44

55
function getA() {
66
return a;
Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,30 @@
1-
// I'm not sure if it's fair calling this a unit test, but let's go with that
2-
import { readdir, readFile } from "node:fs/promises";
3-
import path from "node:path";
41
import { describe, expect, it } from "vitest";
2+
import { getBuildOutputDirs, getFiles, readFileContent } from "~/utils/build-output-utils";
53

64
describe("Make sure treeshaking works", () => {
75
it("should not have any unused code in the client-bundle", async () => {
8-
const buildDir = path.resolve(process.cwd(), ".output/public/_build/assets");
9-
const files = await readdir(buildDir);
10-
const targetFile = files.find(
11-
file => file.startsWith("(no-side-effects)-") && file.endsWith(".js"),
12-
);
13-
if (!targetFile) {
14-
throw new Error("Treeshaking test: No target file not found");
6+
const { clientOutputRoot } = getBuildOutputDirs();
7+
const files = await getFiles(clientOutputRoot, /^\(no-side-effects\)-.*\.js(\.gz|\.br)?$/);
8+
9+
expect(files.length, "No files matching the treeshaking pattern found").toBeGreaterThan(0);
10+
11+
for (const targetFile of files) {
12+
const file = await readFileContent(targetFile);
13+
const result = file.includes("myTreeshakingTestUniqueString1");
14+
expect(result, `Unused code found in file: ${targetFile}`).toBeFalsy();
1515
}
16-
const file = await readFile(path.join(buildDir, targetFile), "utf-8");
16+
});
1717

18-
const regex = /const a = 1;/g;
19-
const result = regex.test(file);
18+
it("should include side-effects code in the client-bundle", async () => {
19+
const { clientOutputRoot } = getBuildOutputDirs();
20+
const files = await getFiles(clientOutputRoot, /^side-effects.*\.js(\.gz|\.br)?$/);
2021

21-
expect(result).toBeFalsy();
22+
expect(files.length, "No side-effects files matching the pattern found").toBeGreaterThan(0);
23+
24+
for (const targetFile of files) {
25+
const file = await readFileContent(targetFile);
26+
const result = file.includes("myTreeshakingTestUniqueString2");
27+
expect(result, `Side-effects code not found in file: ${targetFile}`).toBeTruthy();
28+
}
2229
});
2330
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { existsSync } from "node:fs";
2+
import { readFile, readdir } from "node:fs/promises";
3+
import path from "node:path";
4+
import { brotliDecompressSync, gunzipSync } from "node:zlib";
5+
6+
export function getBuildOutputDirs() {
7+
const appRoot = process.cwd();
8+
const sourceRoot = path.join(appRoot, "src");
9+
const serverOutputRoot = path.join(appRoot, ".output/server");
10+
const clientOutputRoot = path.join(appRoot, ".output/public");
11+
12+
if (!existsSync(sourceRoot)) {
13+
throw new Error(`Source dir not found: ${sourceRoot}`);
14+
}
15+
16+
if (!existsSync(serverOutputRoot)) {
17+
throw new Error(
18+
`Server output dir not found: ${serverOutputRoot}. Did you run the build? (pnpm --filter tests run build)`,
19+
);
20+
}
21+
22+
if (!existsSync(clientOutputRoot)) {
23+
throw new Error(
24+
`Client output dir not found: ${clientOutputRoot}. Did you run the build? (pnpm --filter tests run build)`,
25+
);
26+
}
27+
28+
return {
29+
sourceRoot,
30+
serverOutputRoot,
31+
clientOutputRoot,
32+
};
33+
}
34+
35+
export async function getFiles(dir: string, fileRegex: RegExp): Promise<string[]> {
36+
const entries = await readdir(dir, { recursive: true, withFileTypes: true });
37+
38+
return entries
39+
.filter(e => e.isFile() && fileRegex.test(e.name))
40+
.map(e => path.join(e.parentPath, e.name));
41+
}
42+
43+
export async function readFileContent(filePath: string) {
44+
if (filePath.endsWith(".br")) {
45+
return brotliDecompressSync(await readFile(filePath)).toString("utf-8");
46+
}
47+
48+
if (filePath.endsWith(".gz")) {
49+
return gunzipSync(await readFile(filePath)).toString("utf-8");
50+
}
51+
52+
return readFile(filePath, "utf-8");
53+
}

apps/tests/vitest.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import solid from "vite-plugin-solid";
22
import { defineConfig } from "vitest/config";
33
import { playwright } from "@vitest/browser-playwright";
4+
import path from "path";
45

56
export default defineConfig({
7+
resolve: {
8+
alias: {
9+
"~": path.resolve(__dirname, "./src"),
10+
},
11+
},
612
plugins: [solid()],
713
test: {
814
mockReset: true,

packages/start-nitro-v2-vite-plugin/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export function nitroV2Plugin(nitroConfig?: UserNitroConfig): PluginOption {
8282
generateTsConfig: false,
8383
generateRuntimeConfigTypes: false,
8484
},
85+
compressPublicAssets: true,
8586
...nitroConfig,
8687
dev: false,
8788
routeRules: {

0 commit comments

Comments
 (0)