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/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')), ], 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')), ],