Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/shiny-clowns-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/vite-plugin-nitro-2": patch
---

fix: compress public assets by default, matching `1.x` behavior
20 changes: 20 additions & 0 deletions apps/tests/src/build/compressed-public-assets.server.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, expect, it } from "vitest";
import { getBuildOutputDirs, getFiles } from "~/utils/build-output-utils";

describe("public assets compression", () => {
it("includes at least one .gz and one .br file in client output", async () => {
const { clientOutputRoot } = getBuildOutputDirs();
const gzFiles = await getFiles(clientOutputRoot, /\.gz$/);
const brFiles = await getFiles(clientOutputRoot, /\.br$/);

// Only files above 1KB are compressed, so we check that at least one .gz and one .br file exists
expect(
gzFiles.length,
`No .gz files found in client output: ${clientOutputRoot}`,
).toBeGreaterThan(0);
expect(
brFiles.length,
`No .br files found in client output: ${clientOutputRoot}`,
).toBeGreaterThan(0);
});
});
2 changes: 1 addition & 1 deletion apps/tests/src/routes/treeshaking/(no-side-effects).tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createAsync } from "@solidjs/router";

const a = 1;
const a = "myTreeshakingTestUniqueString1";

function getA() {
return a;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,13 @@
import { existsSync } from "node:fs";
import { readdir, readFile } from "node:fs/promises";
import path from "node:path";
import { brotliDecompressSync, gunzipSync } from "node:zlib";
import { describe, expect, it } from "vitest";
import { getBuildOutputDirs, getFiles, readFileContent } from "~/utils/build-output-utils";

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

describe("server code does not leak to client bundle", () => {
it("verifies secret markers are server-only and not in client output", async () => {
const appRoot = process.cwd();
const sourceRoot = path.join(appRoot, "src");
const serverOutputRoot = path.join(appRoot, ".output/server");
const clientOutputRoot = path.join(appRoot, ".output/public");

// Verify required directories exist
expect(existsSync(sourceRoot), `Source dir not found: ${sourceRoot}`).toBe(true);
expect(
existsSync(serverOutputRoot),
`Server output dir not found: ${serverOutputRoot}. Did you run the build? (pnpm --filter tests run build)`,
).toBe(true);
expect(
existsSync(clientOutputRoot),
`Client output dir not found: ${clientOutputRoot}. Did you run the build? (pnpm --filter tests run build)`,
).toBe(true);
const { sourceRoot, serverOutputRoot, clientOutputRoot } = getBuildOutputDirs();

// Collect and validate markers from source code
const sourceMarkerCounts = await countSourceMarkers(sourceRoot);
Expand Down Expand Up @@ -77,22 +60,3 @@ async function countSourceMarkers(rootDir: string) {
}
return markerCounts;
}

async function getFiles(dir: string, fileRegex: RegExp): Promise<string[]> {
const entries = await readdir(dir, { recursive: true, withFileTypes: true });
return entries
.filter(e => e.isFile() && fileRegex.test(e.name))
.map(e => path.join(e.parentPath, e.name));
}

async function readFileContent(filePath: string) {
if (filePath.endsWith(".br")) {
return brotliDecompressSync(await readFile(filePath)).toString("utf-8");
}

if (filePath.endsWith(".gz")) {
return gunzipSync(await readFile(filePath)).toString("utf-8");
}

return readFile(filePath, "utf-8");
}
2 changes: 1 addition & 1 deletion apps/tests/src/routes/treeshaking/side-effects.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createAsync } from "@solidjs/router";

export const a = 1;
export const a = "myTreeshakingTestUniqueString2";

function getA() {
return a;
Expand Down
35 changes: 21 additions & 14 deletions apps/tests/src/routes/treeshaking/treeshake.server.test.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
// I'm not sure if it's fair calling this a unit test, but let's go with that
import { readdir, readFile } from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { getBuildOutputDirs, getFiles, readFileContent } from "~/utils/build-output-utils";

describe("Make sure treeshaking works", () => {
it("should not have any unused code in the client-bundle", async () => {
const buildDir = path.resolve(process.cwd(), ".output/public/_build/assets");
const files = await readdir(buildDir);
const targetFile = files.find(
file => file.startsWith("(no-side-effects)-") && file.endsWith(".js"),
);
if (!targetFile) {
throw new Error("Treeshaking test: No target file not found");
const { clientOutputRoot } = getBuildOutputDirs();
const files = await getFiles(clientOutputRoot, /^\(no-side-effects\)-.*\.js(\.gz|\.br)?$/);

expect(files.length, "No files matching the treeshaking pattern found").toBeGreaterThan(0);

for (const targetFile of files) {
const file = await readFileContent(targetFile);
const result = file.includes("myTreeshakingTestUniqueString1");
expect(result, `Unused code found in file: ${targetFile}`).toBeFalsy();
}
const file = await readFile(path.join(buildDir, targetFile), "utf-8");
});

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

expect(result).toBeFalsy();
expect(files.length, "No side-effects files matching the pattern found").toBeGreaterThan(0);

for (const targetFile of files) {
const file = await readFileContent(targetFile);
const result = file.includes("myTreeshakingTestUniqueString2");
expect(result, `Side-effects code not found in file: ${targetFile}`).toBeTruthy();
}
});
});
53 changes: 53 additions & 0 deletions apps/tests/src/utils/build-output-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { existsSync } from "node:fs";
import { readFile, readdir } from "node:fs/promises";
import path from "node:path";
import { brotliDecompressSync, gunzipSync } from "node:zlib";

export function getBuildOutputDirs() {
const appRoot = process.cwd();
const sourceRoot = path.join(appRoot, "src");
const serverOutputRoot = path.join(appRoot, ".output/server");
const clientOutputRoot = path.join(appRoot, ".output/public");

if (!existsSync(sourceRoot)) {
throw new Error(`Source dir not found: ${sourceRoot}`);
}

if (!existsSync(serverOutputRoot)) {
throw new Error(
`Server output dir not found: ${serverOutputRoot}. Did you run the build? (pnpm --filter tests run build)`,
);
}

if (!existsSync(clientOutputRoot)) {
throw new Error(
`Client output dir not found: ${clientOutputRoot}. Did you run the build? (pnpm --filter tests run build)`,
);
}

return {
sourceRoot,
serverOutputRoot,
clientOutputRoot,
};
}

export async function getFiles(dir: string, fileRegex: RegExp): Promise<string[]> {
const entries = await readdir(dir, { recursive: true, withFileTypes: true });

return entries
.filter(e => e.isFile() && fileRegex.test(e.name))
.map(e => path.join(e.parentPath, e.name));
}

export async function readFileContent(filePath: string) {
if (filePath.endsWith(".br")) {
return brotliDecompressSync(await readFile(filePath)).toString("utf-8");
}

if (filePath.endsWith(".gz")) {
return gunzipSync(await readFile(filePath)).toString("utf-8");
}

return readFile(filePath, "utf-8");
}
6 changes: 6 additions & 0 deletions apps/tests/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import solid from "vite-plugin-solid";
import { defineConfig } from "vitest/config";
import { playwright } from "@vitest/browser-playwright";
import path from "path";

export default defineConfig({
resolve: {
alias: {
"~": path.resolve(__dirname, "./src"),
},
},
plugins: [solid()],
test: {
mockReset: true,
Expand Down
1 change: 1 addition & 0 deletions packages/start-nitro-v2-vite-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export function nitroV2Plugin(nitroConfig?: UserNitroConfig): PluginOption {
generateTsConfig: false,
generateRuntimeConfigTypes: false,
},
compressPublicAssets: true,
...nitroConfig,
dev: false,
routeRules: {
Expand Down
Loading