Skip to content
Merged
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
29 changes: 29 additions & 0 deletions scripts/compact-css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import postcss from "postcss";

export function compactCss(css: string): string {
const root = postcss.parse(css);
const whitespace = /^[ \t\n\r\f]*$/;
root.walk((node) => {
if (node.type === "rule" || node.type === "atrule") node.raws.semicolon = false;
if (whitespace.test(node.raws.before ?? "")) node.raws.before = "";
if (
(node.type === "rule" || node.type === "atrule") &&
whitespace.test(node.raws.after ?? "")
)
node.raws.after = "";
if (
node.type === "decl" &&
!node.prop.startsWith("--") &&
/^[ \t\n\r\f]*:[ \t\n\r\f]*$/.test(node.raws.between ?? "")
) {
node.raws.between = ":";
} else if (
(node.type === "rule" || node.type === "atrule") &&
whitespace.test(node.raws.between ?? "")
) {
node.raws.between = "";
}
});
if (whitespace.test(root.raws.after ?? "")) root.raws.after = "";
return root.toString();
}
60 changes: 60 additions & 0 deletions tests/tooling/buildOptions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { runInNewContext } from "node:vm";
import { build } from "vite";
import { expect, it, vi } from "vitest";

const root = fileURLToPath(new URL("../..", import.meta.url));

it("preserves getter reads and compact injected CSS through the production build", async () => {
const scratch = path.join(root, ".obsidian-e2e-artifacts");
mkdirSync(scratch, { recursive: true });
const dir = mkdtempSync(path.join(scratch, "build-options-"));
vi.stubEnv("NODE_ENV", "production");
try {
writeFileSync(
path.join(dir, "Component.svelte"),
'<div class="fixture">Example</div><style>.fixture { --empty: ; width: calc(100% - 2px); }</style>',
);
const entry = path.join(dir, "entry.js");
writeFileSync(
entry,
`export { default as Component } from "./Component.svelte";
export function readGetter(object) { object.current; }`,
);
const result = await build({
configFile: path.join(root, "vite.config.ts"),
configLoader: "bundle",
mode: "production",
logLevel: "silent",
plugins: [
{
name: "assert-production",
configResolved(config) {
expect(config.isProduction).toBe(true);
},
},
],
build: { write: false, lib: { entry } },
});
if (!Array.isArray(result)) throw new Error("Expected library build outputs");
const output = result.flatMap((chunk) => chunk.output);
expect(output.map((chunk) => chunk.fileName)).toEqual(["main.js"]);
const chunk = output[0];
if (chunk?.type !== "chunk") throw new Error("Expected a JavaScript chunk");
expect(chunk.code).toMatch(/\.fixture\.pn-[a-z0-9]+\{--empty: ;width:calc\(100% - 2px\)\}/);
const exports: { readGetter?: (object: { current: number }) => void } = {};
runInNewContext(chunk.code, { exports, module: { exports } });
let reads = 0;
exports.readGetter?.({
get current() {
return ++reads;
},
});
expect(reads).toBe(1);
} finally {
vi.unstubAllEnvs();
rmSync(dir, { recursive: true, force: true });
}
}, 20_000);
49 changes: 49 additions & 0 deletions tests/tooling/compactCss.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { compile, preprocess } from "svelte/compiler";
import { describe, expect, it } from "vitest";
import { stripCssComments } from "../../scripts/css-comments";
import { compactCss } from "../../scripts/compact-css";

describe("compactCss", () => {
it("removes formatting around rules, at-rules, and ordinary declarations", () => {
expect(compactCss("@media (width > 10px) { .a \n\t{ color: red; } }")).toBe(
"@media (width > 10px){.a{color:red}}",
);
});

it.each([
['.a .b { content: "two words"; }', '.a .b{content:"two words"}'],
[".a { width: calc(100% - 2px); }", ".a{width:calc(100% - 2px)}"],
[".a { --empty: ; --tokens: one two; }", ".a{--empty: ;--tokens: one two}"],
[".a { background: url(/a/*b*/c.png); }", ".a{background:url(/a/*b*/c.png)}"],
[".a /**/ .b { color: r/**/ed; }", ".a /**/ .b{color:r/**/ed}"],
["/*! Keep this */ .a { color: red; }", "/*! Keep this */.a{color:red}"],
["@import url(example.css) screen;", "@import url(example.css) screen;"],
[".a\u00a0{ color: red; }", ".a\u00a0{color:red}"],
[".a { --empty: ; }", ".a{--empty: }"],
[".a /* keep */ { color: red; }", ".a /* keep */ {color:red}"],
[".a { color /* keep */ : red; }", ".a{color /* keep */ : red}"],
[".a { color: red; background: blue; }", ".a{color:red;background:blue}"],
[".a { color: red; /*! keep */ }", ".a{color:red/*! keep */}"],
[" \n\t", ""],
])("preserves tokens in %s", (input, expected) => {
expect(compactCss(input)).toBe(expected);
expect(compactCss(expected)).toBe(expected);
});

it("compiles compact CSS into the injected stylesheet without changing values", async () => {
const source = `<div class="a">Example</div><style>
.a { --empty: ; width: calc(100% - 2px); }
@media (min-width: 10px) { .a { color: red; } }
</style>`;
const processed = await preprocess(source, {
style: ({ content }) => ({ code: compactCss(stripCssComments(content)) }),
});
const result = compile(processed.code, {
css: "injected",
cssHash: () => "podnotes-test",
});
expect(result.js.code).toContain(".a.podnotes-test{--empty: ;width:calc(100% - 2px)}");
expect(result.js.code).toContain("@media (min-width: 10px){.a.podnotes-test{color:red}");
expect(result.js.code).toContain("$.append_styles");
});
});
10 changes: 8 additions & 2 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { svelte } from "@sveltejs/vite-plugin-svelte";
import { defineConfig } from "vite";
import { createBuildArtifactPlugin } from "./scripts/build-artifacts.ts";
import { stripCssComments } from "./scripts/css-comments.ts";
import { compactCss } from "./scripts/compact-css.ts";
import { svelteWithoutHydration } from "./scripts/svelte-without-hydration.ts";
import { syntaxFloor } from "./scripts/syntax-floor.ts";

Expand Down Expand Up @@ -60,7 +61,12 @@ export default defineConfig(({ mode }) => {
plugins: [
svelte({
// Svelte copies style comments verbatim into the CSS it injects from main.js.
preprocess: { style: ({ content }) => ({ code: stripCssComments(content) }) },
preprocess: {
style: ({ content }) => {
const css = stripCssComments(content);
return { code: isProd ? compactCss(css) : css };
},
},
compilerOptions: {
css: "injected",
discloseVersion: false,
Expand Down Expand Up @@ -96,7 +102,7 @@ export default defineConfig(({ mode }) => {
// before the Oxc minifier below, which mangles names and drops console calls.
minify: isProd ? "terser" : false,
// Reading a Svelte state property subscribes to it, so a read is not pure.
terserOptions: { compress: { pure_getters: false }, mangle: false },
terserOptions: { compress: { pure_getters: false, passes: 2 }, mangle: false },
rollupOptions: {
external,
output: {
Expand Down
Loading