From 1fa070fece6ac610147c5c6f2e62ed8a96f4afc9 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Thu, 3 Sep 2026 17:38:54 +0800 Subject: [PATCH 1/2] fix(desktop): reload Vite when workspace exports change Workspace manifests live outside the renderer watch root, and ordinary file invalidation leaves Vite's native package resolver cache stale. Register those manifests as configuration dependencies so Vite owns watching and restarting. Keep package exports authoritative and retain the existing workspace build flow. Generated-by: Codex --- apps/desktop/package.json | 2 +- .../scripts/vite-workspace-packages.test.mjs | 81 +++++++++++++++++++ apps/desktop/vite-workspace-packages.ts | 39 +++++++++ apps/desktop/vite.config.ts | 2 + 4 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 apps/desktop/scripts/vite-workspace-packages.test.mjs create mode 100644 apps/desktop/vite-workspace-packages.ts diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 2d396dab4b..6240a1c749 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -41,7 +41,7 @@ "package:linux-deb-arm64": "electron-builder --config electron-builder.config.mjs --linux deb --arm64 --publish never", "typecheck": "tsc -p tsconfig.preload.json --noEmit && tsc -p tsconfig.main.json --noEmit && tsc -p tsconfig.renderer.json --noEmit && tsc -p tsconfig.storybook.json --noEmit", "typecheck:stories": "tsc -p tsconfig.storybook.json --noEmit", - "test:dist": "node --test \"dist/main/**/*.test.js\" scripts/dev-app-runtime.test.mjs", + "test:dist": "node --test \"dist/main/**/*.test.js\" scripts/dev-app-runtime.test.mjs scripts/vite-workspace-packages.test.mjs", "e2e": "npm run build:with-deps && playwright test --config e2e/playwright.config.ts", "build:with-deps": "npm run build:workspace-deps && npm run build", "smoke:real-window": "npm run build:with-deps && node ../../scripts/desktop-real-window-smoke.mjs", diff --git a/apps/desktop/scripts/vite-workspace-packages.test.mjs b/apps/desktop/scripts/vite-workspace-packages.test.mjs new file mode 100644 index 0000000000..6b54f44fe2 --- /dev/null +++ b/apps/desktop/scripts/vite-workspace-packages.test.mjs @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import { mkdir, mkdtemp, realpath, rm, symlink, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { setTimeout } from 'node:timers/promises'; +import { test } from 'node:test'; +import { createServer } from 'vite'; +import { workspacePackagesPlugin } from '../vite-workspace-packages.ts'; + +test('renderer loads a newly exported workspace module after its manifest changes', async (t) => { + const repoRoot = await realpath(await mkdtemp(join(tmpdir(), 'maka-workspace-exports-'))); + let server; + t.after(async () => { + await server?.close(); + await rm(repoRoot, { recursive: true, force: true }); + }); + const root = join(repoRoot, 'apps/desktop/src/renderer'); + const core = join(repoRoot, 'packages/core'); + await mkdir(root, { recursive: true }); + await mkdir(join(core, 'dist'), { recursive: true }); + await mkdir(join(repoRoot, 'node_modules/@maka'), { recursive: true }); + await symlink(core, join(repoRoot, 'node_modules/@maka/core'), 'junction'); + await writeFile(join(repoRoot, 'package.json'), JSON.stringify({ workspaces: ['packages/core'] })); + const manifest = { name: '@maka/core', type: 'module', exports: { './session': './dist/session.js' } }; + await writeFile(join(core, 'package.json'), JSON.stringify(manifest)); + await writeFile(join(core, 'dist/session.js'), 'export const session = 1;'); + await writeFile(join(root, 'entry.js'), "export { session } from '@maka/core/session';"); + + server = await createServer({ + configFile: false, + root, + logLevel: 'silent', + server: { host: '127.0.0.1', port: 0 }, + optimizeDeps: { noDiscovery: true, include: [] }, + plugins: [workspacePackagesPlugin(repoRoot)], + }); + await server.listen(); + const url = server.resolvedUrls.local[0]; + assert.equal((await fetch(`${url}entry.js`)).status, 200); + + // The workspace build has emitted the new module before the manifest changes. + await writeFile(join(core, 'dist/workhub-session-resolver.js'), 'export const resolver = 2;'); + manifest.exports['./workhub-session-resolver'] = './dist/workhub-session-resolver.js'; + await writeFile(join(core, 'package.json'), JSON.stringify(manifest)); + await writeFile(join(root, 'entry.js'), "export { resolver } from '@maka/core/workhub-session-resolver';"); + + let failure; + const deadline = Date.now() + 5000; + while (Date.now() < deadline) { + try { + const response = await fetch(`${url}entry.js`); + const body = await response.text(); + assert.equal(response.status, 200, body); + assert.match(body, /\/packages\/core\/dist\/workhub-session-resolver\.js/); + return; + } catch (error) { + failure = error; + await setTimeout(50); + } + } + throw failure; +}); diff --git a/apps/desktop/vite-workspace-packages.ts b/apps/desktop/vite-workspace-packages.ts new file mode 100644 index 0000000000..19a06f7ab3 --- /dev/null +++ b/apps/desktop/vite-workspace-packages.ts @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { normalizePath, type Plugin } from 'vite'; + +export function workspacePackagesPlugin(repoRoot: string): Plugin { + return { + name: 'maka-workspace-packages', + apply: 'serve', + configResolved(config) { + const manifest = resolve(repoRoot, 'package.json'); + const { workspaces } = JSON.parse(readFileSync(manifest, 'utf8')) as { workspaces: string[] }; + // Workspace exports change resolution just like Vite config does. A file + // watch alone leaves the native resolver's package cache stale. + config.configFileDependencies.push( + normalizePath(manifest), + ...workspaces.map((workspace) => normalizePath(resolve(repoRoot, workspace, 'package.json'))), + ); + }, + }; +} diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts index 23b8539fba..aa75bc7fbd 100644 --- a/apps/desktop/vite.config.ts +++ b/apps/desktop/vite.config.ts @@ -24,6 +24,7 @@ import react from '@vitejs/plugin-react'; import { dependencyPatchesCachePlugin } from './vite-dependency-patches.js'; import { bundledNpmPackagesPlugin } from './vite-bundled-packages.js'; import { rendererEntryContractPlugin } from './scripts/vite-renderer-entry-contract.js'; +import { workspacePackagesPlugin } from './vite-workspace-packages.js'; /** * PR-ICONS-FULL-REPLACE-0 (WAWQAQ msg `60064e2d` 2026-06-24): point the @@ -46,6 +47,7 @@ export default defineConfig({ plugins: [ react(), dependencyPatchesCachePlugin(REPO_ROOT), + workspacePackagesPlugin(REPO_ROOT), bundledNpmPackagesPlugin(), rendererEntryContractPlugin(resolve(import.meta.dirname, 'src/renderer')), ], From fa8f803d484a74f1b89d6daf1c79745a6764c0aa Mon Sep 17 00:00:00 2001 From: AstroHan Date: Thu, 3 Sep 2026 17:59:07 +0800 Subject: [PATCH 2/2] fix(desktop): register workspace plugin in renderer architecture contract The renderer entry guard pins Vite plugin identity, arguments, and order. Include the workspace export invalidation plugin in that contract while preserving the entry and output restrictions. Update the existing valid-entry fixture to exercise the production plugin list. Generated-by: Codex --- apps/desktop/scripts/check-renderer-architecture.mjs | 10 +++++++--- .../scripts/check-renderer-architecture.test.mjs | 2 ++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/desktop/scripts/check-renderer-architecture.mjs b/apps/desktop/scripts/check-renderer-architecture.mjs index da6f6934c2..b59400f62c 100644 --- a/apps/desktop/scripts/check-renderer-architecture.mjs +++ b/apps/desktop/scripts/check-renderer-architecture.mjs @@ -2125,8 +2125,9 @@ function validateViteEntryContract(desktopRoot, violations) { }; const reactCall = pluginCall(0, 'react', 0); const dependencyPatchesCall = pluginCall(1, 'dependencyPatchesCachePlugin', 1); - const bundledPackagesCall = pluginCall(2, 'bundledNpmPackagesPlugin', 0); - const rendererContractCall = pluginCall(3, 'rendererEntryContractPlugin', 1); + const workspacePackagesCall = pluginCall(2, 'workspacePackagesPlugin', 1); + const bundledPackagesCall = pluginCall(3, 'bundledNpmPackagesPlugin', 0); + const rendererContractCall = pluginCall(4, 'rendererEntryContractPlugin', 1); const rendererContractRoot = unwrapExpression(rendererContractCall?.arguments[0]); const hasPinnedRendererContractRoot = rendererContractRoot?.type === 'CallExpression' && @@ -2139,6 +2140,7 @@ function validateViteEntryContract(desktopRoot, violations) { hasNamedImport(program, 'node:path', 'resolve') && hasDefaultImport(program, '@vitejs/plugin-react', 'react') && hasNamedImport(program, './vite-dependency-patches.js', 'dependencyPatchesCachePlugin') && + hasNamedImport(program, './vite-workspace-packages.js', 'workspacePackagesPlugin') && hasNamedImport(program, './vite-bundled-packages.js', 'bundledNpmPackagesPlugin') && hasNamedImport( program, @@ -2146,10 +2148,12 @@ function validateViteEntryContract(desktopRoot, violations) { 'rendererEntryContractPlugin', ); const hasPinnedPlugins = - pluginElements.length === 4 && + pluginElements.length === 5 && Boolean(reactCall) && Boolean(dependencyPatchesCall) && isIdentifier(dependencyPatchesCall.arguments[0], 'REPO_ROOT') && + Boolean(workspacePackagesCall) && + isIdentifier(workspacePackagesCall.arguments[0], 'REPO_ROOT') && Boolean(bundledPackagesCall) && Boolean(rendererContractCall) && hasPinnedRendererContractRoot; diff --git a/apps/desktop/scripts/check-renderer-architecture.test.mjs b/apps/desktop/scripts/check-renderer-architecture.test.mjs index 41aea4b1e7..e6043da2b0 100644 --- a/apps/desktop/scripts/check-renderer-architecture.test.mjs +++ b/apps/desktop/scripts/check-renderer-architecture.test.mjs @@ -232,12 +232,14 @@ function rendererEntryContractFiles(overrides = {}) { import { rendererEntryContractPlugin } from './scripts/vite-renderer-entry-contract.js'; import { bundledNpmPackagesPlugin } from './vite-bundled-packages.js'; import { dependencyPatchesCachePlugin } from './vite-dependency-patches.js'; + import { workspacePackagesPlugin } from './vite-workspace-packages.js'; const REPO_ROOT = '/fixture'; export default defineConfig({ root: 'src/renderer', plugins: [ react(), dependencyPatchesCachePlugin(REPO_ROOT), + workspacePackagesPlugin(REPO_ROOT), bundledNpmPackagesPlugin(), rendererEntryContractPlugin(resolve(import.meta.dirname, 'src/renderer')), ],