slidev-addon-anipres
Version:
173 lines (162 loc) • 6.6 kB
text/typescript
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import { defineConfig, normalizePath, type Plugin } from "vite";
import Font from "vite-plugin-font";
import regexpEscape from "regexp.escape";
const fontDirPath = normalizePath(path.join(__dirname, "./assets/fonts/"));
const xiaolaiFontPath = normalizePath(
path.join(fontDirPath, "XiaolaiSC-Regular.ttf"),
);
// In the production build, the font path passed to the font plugin is a relative path like "../../node_modules/...", not a full path like "/Users/.../node_modules/...".
// So we need to omit the path segment before `node_modules` from the query regex to match the relative path.
const fontDirPathFromNodeModules = fontDirPath.replace(
/^.*(?=\/node_modules\/)/,
"",
);
const fontDirRegex = new RegExp(
regexpEscape(fontDirPathFromNodeModules) + ".*\\.ttf(\\?.*)?$",
);
let root = process.cwd();
function resolveSnapshotPath() {
return path.join(root, ".slidev/anipres/snapshots");
}
function isPathIn(target: string, maybeParent: string) {
const relative = path.relative(maybeParent, target);
return relative && !relative.startsWith("..") && !path.isAbsolute(relative);
}
export default defineConfig(({ mode }) => ({
optimizeDeps: {
/*
* In dev mode, Vite serves native ESM, so CommonJS packages like "react" don't expose named exports (e.g., useCallback) correctly,
* which causes runtime errors like 'The requested module ... does not provide an export named ...'
* when this package is used in the dev mode of Slidev.
* Configuring `optimizeDeps` here tells Vite to pre-bundle these dependencies so that they behave as ES modules.
*
* References:
* - Vite Dep Pre-Bundling: https://vitejs.dev/guide/dep-pre-bundling.html
*/
include: [
"slidev-addon-anipres > react",
"slidev-addon-anipres > react-dom",
"slidev-addon-anipres > react-dom/client",
"slidev-addon-anipres > tldraw",
],
},
plugins: [
{
name: "set-font-subsets",
resolveId(id) {
if (id === "/@xiaolai-font.ttf") {
console.debug(`Resolve ${id} as ${xiaolaiFontPath}`);
// Enable extremely lightweight optimization (https://www.npmjs.com/package/vite-plugin-font#extremely-lightweight-optimization)
// by adding `?subsets` to the font URL only in production mode.
// This setting is effective in combination with the `scanFiles` option of the `Font.vite` plugin below.
// We use this plugin-based approach to modify the module name dynamically at build time.
return xiaolaiFontPath + (mode === "production" ? "?subsets" : "");
}
},
},
Font.vite({
// Enable optimization only in production mode. See the comment in the `resolveId` hook above.
scanFiles:
mode === "production"
? ["**/.slidev/anipres/snapshots/*.json"]
: undefined,
// `node_modules` is excluded by default, which is not good in our case where the font file will be in `node_modules` when this package is installed to user's environment by a package manager.
// So we unset the `exclude` option to override the default behavior.
exclude: [],
// Also we set a stricter include path explicitly to avoid unexpected side effects from setting `exclude` to `[]`.
include: [fontDirRegex],
}) as Plugin,
{
// Load and save Tldraw snapshots from/to the file system via Vite plugin.
// This implementation is based on slidev-addon-graph:
// https://github.com/antfu/slidev-addon-graph/blob/5c7dbfbf198c401477f9b50ce4de4e9e50243d16/vite.config.ts
name: "anipres-server",
configureServer(server) {
root = server.config.root;
server.ws.on("connection", (socket) => {
socket.on("message", async (data) => {
const payload = JSON.parse(data.toString());
if (
payload.type === "custom" &&
payload.event === "anipres-snapshot"
) {
const snapshotDir = resolveSnapshotPath();
const snapshotData = JSON.stringify(
payload.data.snapshot,
null,
2,
);
fs.mkdirSync(snapshotDir, { recursive: true });
fs.writeFileSync(
path.join(snapshotDir, `${payload.data.id}.json`),
snapshotData,
);
// Invalidate the module so that the saved snapshot is loaded on the next request.
const mod = server.moduleGraph.getModuleById(
"/@slidev-anipres-snapshot",
);
if (mod) server.moduleGraph.invalidateModule(mod);
}
});
});
},
configResolved(config) {
root = config.root;
},
resolveId(id) {
if (id === "/@slidev-anipres-snapshot") {
return id;
}
},
load(id) {
if (id === "/@slidev-anipres-snapshot") {
const snapshotPath = resolveSnapshotPath();
const files = fs.existsSync(snapshotPath)
? fs.readdirSync(snapshotPath)
: [];
return [
"",
...files.map((file, idx) => {
return `import v${idx} from ${JSON.stringify(normalizePath(path.join(snapshotPath, file)))}`;
}),
"const snapshots = {",
files
.map((file, idx) => {
return ` ${JSON.stringify(file.replace(/\.json$/, ""))}: v${idx}`;
})
.join(",\n"),
"}",
"export default snapshots",
"if (import.meta.hot) {",
" import.meta.hot.accept(({ default: newSnapshots }) => {",
" Object.assign(snapshots, newSnapshots)",
" })",
"}",
].join("\n");
}
},
},
],
build: {
rollupOptions: {
onwarn(warning, warn) {
// Vite reports a missing export just as a warning,
// but we want to treat it as an error so that the build fails
// in the case where the missing export is a font file.
// because it leads to an error and broken styles at runtime.
if (warning.code === "MISSING_EXPORT") {
if (
warning.exporter &&
isPathIn(normalizePath(warning.exporter), fontDirPath)
) {
throw new Error(`Build failed due to: ${warning.message}`);
}
}
warn(warning);
},
},
},
}));