Reduce production bundles while preserving host registration

This commit is contained in:
vorotamoroz committed 2026-10-29 15:06:33 +00:00
1 parent b9dcb0d4c7
commit b8d4ff2c16
10 files changed
+133 -20

No files matched your search

+6
View File
@@ -47,6 +47,12 @@ npm run test:e2e:browser-apps:interop # Run WebApp → WebPeer → CLI in Com
npm run test:e2e:obsidian:local-suite # Run the real Obsidian local suite
```
### Production minification
Production builds remove unused declarations and shorten local identifiers while retaining class and function names used by module discovery and diagnostic stacks. `terser.config.mjs` also preserves property names, top-level bindings, and identifiers visible to direct `eval` calls. Keep these boundaries when changing compression settings; `test/build/terserConfig.unit.spec.ts` checks the generated code against host properties, persisted keys, reflective names, and exception stacks.
Compare bundle bytes and gzip bytes from the same source, dependency artefacts, and build inputs. Validate import-time registration and the resulting plug-in in the affected real-runtime workflows. The iOS syntax check is one part of compatibility verification; it does not measure performance or replace physical-device testing.
### Tips
Select the narrowest unit, integration, CLI E2E, or real Obsidian E2E command that owns the behaviour being changed. The obsolete mocked browser Harness has been retired.
+1 -1
View File
@@ -194,7 +194,7 @@ const context = await esbuild.context({
platform: "browser",
metafile: true,
sourcemap: prod ? false : "inline",
treeShaking: false,
treeShaking: true,
outfile: "main_org.js",
mainFields: ["browser", "module", "main"],
minifyWhitespace: false,
+7
View File
@@ -120,6 +120,13 @@ export default defineConfig([
"@typescript-eslint/restrict-template-expressions": "warn",
},
},
{
files: ["src/apps/cli/vite.config.ts", "src/apps/webapp/vite.config.ts"],
rules: {
// Vite loads these configurations before the application source alias is available.
"livesync-imports/prefer-source-alias": "off",
},
},
{
files: ["src/apps/browser/**/*.{ts,svelte}", "src/apps/webapp/**/*.ts"],
rules: {
+2 -9
View File
@@ -1,18 +1,11 @@
import { defaultServerConditions, defaultServerMainFields, defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { fileURLToPath, fs, isBuiltin, path } from "@vrtmrz/livesync-commonlib/node";
import { fileURLToPath, isBuiltin, path } from "@vrtmrz/livesync-commonlib/node";
import { readVersion } from "../readVersion";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const resolve = (...args: string[]) => path.resolve(...args).replace(/\\/g, "/");
const repoRoot = path.resolve(__dirname, "../../..");
function readVersion(filePath: string): string | undefined {
const parsed: unknown = JSON.parse(fs.readFileSync(filePath, "utf-8"));
if (typeof parsed !== "object" || parsed === null || !("version" in parsed)) {
return undefined;
}
return typeof parsed.version === "string" ? parsed.version : undefined;
}
const packageVersion = readVersion(path.resolve(repoRoot, "package.json"));
const manifestVersion = readVersion(path.resolve(repoRoot, "manifest.json"));
// https://vite.dev/config/
+10
View File
@@ -0,0 +1,10 @@
import { fs } from "@vrtmrz/livesync-commonlib/node";
/** Read a string-valued version field from a JSON file during Node builds. */
export function readVersion(filePath: string): string | undefined {
const parsed: unknown = JSON.parse(fs.readFileSync(filePath, "utf-8"));
if (typeof parsed !== "object" || parsed === null || !("version" in parsed)) {
return undefined;
}
return typeof parsed.version === "string" ? parsed.version : undefined;
}
+36
View File
@@ -0,0 +1,36 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { readVersion } from "./readVersion";
describe("readVersion", () => {
let directory: string;
beforeEach(() => {
directory = mkdtempSync(join(tmpdir(), "livesync-read-version-"));
});
afterEach(() => {
rmSync(directory, { recursive: true, force: true });
});
it("returns only string version values", () => {
const filePath = join(directory, "version.json");
writeFileSync(filePath, JSON.stringify({ version: "1.2.3" }));
expect(readVersion(filePath)).toBe("1.2.3");
for (const contents of ["null", "[]", "{}", '{"version": 42}', '{"version": null}']) {
writeFileSync(filePath, contents);
expect(readVersion(filePath)).toBeUndefined();
}
});
it("preserves file read and JSON parse errors", () => {
expect(() => readVersion(join(directory, "missing.json"))).toThrow();
const filePath = join(directory, "invalid.json");
writeFileSync(filePath, "not JSON");
expect(() => readVersion(filePath)).toThrow(SyntaxError);
});
});
+2 -9
View File
@@ -1,17 +1,10 @@
import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { fileURLToPath, fs, path } from "@vrtmrz/livesync-commonlib/node";
import { fileURLToPath, path } from "@vrtmrz/livesync-commonlib/node";
import { readVersion } from "../readVersion";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, "../../..");
function readVersion(filePath: string): string | undefined {
const parsed: unknown = JSON.parse(fs.readFileSync(filePath, "utf-8"));
if (typeof parsed !== "object" || parsed === null || !("version" in parsed)) {
return undefined;
}
return typeof parsed.version === "string" ? parsed.version : undefined;
}
const packageVersion = readVersion(path.resolve(repoRoot, "package.json"));
const manifestVersion = readVersion(path.resolve(repoRoot, "manifest.json"));
// https://vite.dev/config/
+6 -1
View File
@@ -60,7 +60,12 @@ const terserOption = {
hoist_vars: true,
// toplevel: "vars",
},
mangle: false,
// Keep property keys, reflective names, and eval-visible bindings stable.
mangle: {
properties: false,
toplevel: false,
eval: false,
},
ecma: 2018, // specify one of: 5, 2015, 2016, etc.
enclose: false, // or specify true, or "args:values"
+58
View File
@@ -0,0 +1,58 @@
import { runInNewContext } from "node:vm";
import { describe, expect, it } from "vitest";
import { minify } from "terser";
import { terserOption } from "../../terser.config.mjs";
const fixture = `
const host = require("obsidian");
class ModuleLog {
constructor(configuration) { this.savedSettings = configuration; }
read(longParameterName) {
const longIntermediateValue = host.prefix + longParameterName;
return { savedSettings: this.savedSettings, message: longIntermediateValue };
}
}
function renderMessage(inputValue) { return new ModuleLog({ persistedField: "unchanged" }).read(inputValue); }
function readWithEval(longEvalParameter) { return eval("longEvalParameter"); }
function failWithName() { throw new Error("fixture failure"); }
module.exports = { ModuleLog, renderMessage, readWithEval, failWithName };
`;
describe("production minifier", () => {
it("shortens local identifiers while preserving host properties, module names, and diagnostics", async () => {
const result = await minify(fixture, { ...terserOption, sourceMap: false });
expect(result.code).toBeDefined();
const sandbox = {
module: {
exports: {} as {
ModuleLog: { name: string };
renderMessage(input: string): unknown;
readWithEval(input: string): string;
failWithName(): never;
},
},
require: (name: string) => {
expect(name).toBe("obsidian");
return { prefix: "host:" };
},
};
runInNewContext(result.code!, sandbox);
const api = sandbox.module.exports;
expect(api.ModuleLog.name).toBe("ModuleLog");
expect(api.renderMessage.name).toBe("renderMessage");
expect(api.renderMessage("sample")).toEqual({
savedSettings: { persistedField: "unchanged" },
message: "host:sample",
});
expect(api.readWithEval("evaluated")).toBe("evaluated");
let failure: Error | undefined;
try {
api.failWithName();
} catch (error) {
failure = error as Error;
}
expect(failure?.stack).toContain("failWithName");
expect(result.code).not.toContain("longParameterName");
expect(result.code).not.toContain("longIntermediateValue");
});
});
+5
View File
@@ -12,6 +12,11 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi
## Unreleased
### Maintenance
- The plug-in bundle is smaller, reducing the download for installation and updates.
- Builds remove unused code and shorten local identifiers while retaining class and function names for diagnostics.
### Diagnostics
- Service log calls stay with the plug-in instance that registered them. Its log connection is released after the full shutdown, so stale instances cannot receive later service messages.