Skip to content
Closed
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/vite-cache-dir.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@open-slide/core': patch
---

Store Vite's optimize-deps cache at the project root and clear it on in-app update, so upgrading no longer leaves the dev server failing on missing `.vite/deps` chunks.
10 changes: 10 additions & 0 deletions packages/core/src/vite/cache-dir.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import path from 'node:path';
import { describe, expect, it } from 'vitest';
import { resolveViteCacheDir } from './cache-dir.ts';

describe('resolveViteCacheDir', () => {
it('points at the user project root, not the installed core package', () => {
const cwd = path.join(path.sep, 'Users', 'david', 'slides');
expect(resolveViteCacheDir(cwd)).toBe(path.join(cwd, 'node_modules', '.vite'));
});
});
10 changes: 10 additions & 0 deletions packages/core/src/vite/cache-dir.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import path from 'node:path';

// Vite defaults its optimize-deps cache to `<nearest package.json>/node_modules/.vite`.
// Our `root` points inside the installed @open-slide/core package, so that default
// lands the cache under node_modules/@open-slide/core/node_modules/.vite — inside the
// very directory the in-app updater swaps out on upgrade, leaving Vite referencing
// chunks that no longer exist. Pin it to the user's project root instead.
export function resolveViteCacheDir(userCwd: string): string {
return path.join(userCwd, 'node_modules', '.vite');
}
2 changes: 2 additions & 0 deletions packages/core/src/vite/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import type { InlineConfig } from 'vite';
import { apiPlugin } from './api-plugin.ts';
import { resolveViteCacheDir } from './cache-dir.ts';
import { currentPlugin } from './current-plugin.ts';
import { designPlugin } from './design-plugin.ts';
import { locTagsPlugin } from './loc-tags-plugin.ts';
Expand Down Expand Up @@ -54,6 +55,7 @@ export async function createViteConfig(opts: CreateViteConfigOptions): Promise<I
return {
base: config.base ?? '/',
root: APP_ROOT,
cacheDir: resolveViteCacheDir(userCwd),
configFile: false,
envDir: userCwd,
plugins: [
Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/vite/routes/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import fs from 'node:fs/promises';
import path from 'node:path';
import type { ViteDevServer } from 'vite';
import { validateMutationRequest } from '../../http/request-guard.ts';
import { resolveViteCacheDir } from '../cache-dir.ts';
import { type ApiContext, json } from './context.ts';

// GET /__update-check → { current, latest, outdated }
Expand Down Expand Up @@ -143,13 +144,26 @@ async function runCommand(spec: CommandSpec, cwd: string): Promise<void> {
});
}

// Drop Vite's optimize-deps cache so the post-update restart re-bundles against the
// freshly installed version. Vite keys that cache off a dep hash that doesn't reflect
// an @open-slide/core version bump (notably it can't read bun's text lockfile), so
// without this it keeps serving the previous version's deps. Best-effort: on Windows
// the outgoing server may still hold the files, and a stale cache only costs a manual
// restart, never correctness.
async function clearViteCache(cwd: string): Promise<void> {
try {
await fs.rm(resolveViteCacheDir(cwd), { recursive: true, force: true });
} catch {}
}

async function updatePackage(ctx: ApiContext): Promise<UpdateResult> {
const packageManager = await detectPackageManager(ctx.userCwd);
const updateCommand = updateCommandFor(packageManager);
const syncCommand = localOpenSlideCommand(ctx.userCwd);

await runCommand(updateCommand, ctx.userCwd);
await runCommand(syncCommand, ctx.userCwd);
await clearViteCache(ctx.userCwd);
Comment on lines +147 to +166

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate update.ts =="
fd -a 'update\.ts$' . | sed 's#^\./##'

echo "== file outline =="
ast-grep outline packages/core/src/vite/routes/update.ts --view expanded || true

echo "== relevant lines 1-240 =="
sed -n '1,240p' packages/core/src/vite/routes/update.ts | cat -n

echo "== search vite cache dir / normalize function symbols =="
rg -n "resolveViteCacheDir|createViteConfig|userCwd|clearViteCache|sync:skills|runCommand" packages/core/src packages/core -S

echo "== package manager / changeset context =="
git diff --stat || true
git status --short || true
fd 'package.json' . | sed -n '1,20p'

Repository: 1weiho/open-slide

Length of output: 27346


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Behavioral probe: model the update flow control and directory resolution without running repo code.
node - <<'JS'
function currentFlow(updateOk, syncStatus, currentDir) {
  const reachedClear = syncStatus === 'ok';
  const cleanupTarget = currentDir;
  return { reachedClear, cleanupTarget };
}

function proposedFlow(updateOk, syncStatus, currentDir, rootDir) {
  try {
    if (syncStatus !== 'ok') throw new Error('sync failed');
    return { caught: false, reachedClear: true, cleanupTarget: rootDir };
  } catch {
    return { caught: true, reachedClear: true, cleanupTarget: rootDir };
  }
}

const cases = [
  { case: 'sync ok no change', flow: currentFlow(true, 'ok', 'relativeCwd'), expected: { reachedClear: true, cleanupTarget: 'relativeCwd' } },
  { case: 'sync fail no change', flow: currentFlow(true, 'fail', 'relativeCwd'), expected: { reachedClear: false, cleanupTarget: 'relativeCwd' } },
  { case: 'sync fail proposed', flow: proposedFlow(true, 'fail', 'relativeCwd', '/home/project'), expected: { reachedClear: true, cleanupTarget: '/home/project' } },
];

for (const {case: label, flow, expected} of cases) {
  const ok = JSON.stringify(flow) === JSON.stringify(expected);
  console.log(label, ok ? 'ok' : 'mismatch', JSON.stringify(flow), 'expect', JSON.stringify(expected));
}
JS

Repository: 1weiho/open-slide

Length of output: 580


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Behavioral probe: model the update flow control and directory resolution without running repo code.
node - <<'JS'
function currentFlow(updateOk, syncStatus, currentDir) {
  const reachedClear = syncStatus === 'ok';
  const cleanupTarget = currentDir;
  return { reachedClear, cleanupTarget };
}

function proposedFlow(updateOk, syncStatus, currentDir, rootDir) {
  try {
    if (syncStatus !== 'ok') throw new Error('sync failed');
    return { caught: false, reachedClear: true, cleanupTarget: rootDir };
  } catch {
    return { caught: true, reachedClear: true, cleanupTarget: rootDir };
  }
}

const cases = [
  { case: 'sync ok no change', flow: currentFlow(true, 'ok', 'relativeCwd'), expected: { reachedClear: true, cleanupTarget: 'relativeCwd' } },
  { case: 'sync fail no change', flow: currentFlow(true, 'fail', 'relativeCwd'), expected: { reachedClear: false, cleanupTarget: 'relativeCwd' } },
  { case: 'sync fail proposed', flow: proposedFlow(true, 'fail', 'relativeCwd', '/home/project'), expected: { caught: true, reachedClear: true, cleanupTarget: '/home/project' } },
];

for (const {case: label, flow, expected} of cases) {
  const ok = JSON.stringify(flow) === JSON.stringify(expected);
  console.log(label, ok ? 'ok' : 'mismatch', JSON.stringify(flow), 'expect', JSON.stringify(expected));
}
JS

Repository: 1weiho/open-slide

Length of output: 588


Make cache invalidation unconditional after installation.

If sync:skills fails after the package install succeeds, execution never reaches clearViteCache(ctx.userCwd), leaving the new core package paired with stale optimized dependencies. Run cache cleanup from a finally block and normalize ctx.userCwd using the same resolved project root used by Vite config resolution; surface cleanup failures or inform the user that a manual restart is required.

stability_and_availability

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from 'node:child_process';
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/vite/routes/update.ts` around lines 147 - 166, Update
updatePackage so clearViteCache always runs after installation by placing the
syncCommand execution in a try/finally block, and normalize ctx.userCwd to the
resolved project root used by Vite configuration resolution before cleanup. Do
not silently swallow cache-removal failures: surface the cleanup error or notify
the user that a manual restart is required.


cache = null;
const latest = await fetchLatest(Date.now());
Expand Down
Loading