Skip to content
Merged
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
21 changes: 20 additions & 1 deletion apps/desktop/electron-builder.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,35 @@
* under the License.
*/

import { readFileSync } from 'node:fs';
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import {
DESKTOP_NIGHTLY_FEED_URL,
resolveDesktopBuildVersion,
resolveRuntimeHostSetupPackage,
} from '../../scripts/desktop-nightly.mjs';
import { workspaceReleaseManifest } from '../../scripts/release-cli-file-policy.mjs';
import { resolveProductManifestIdentity } from '../../scripts/product-release-identity.mjs';

function readManifest(relativePath) {
return JSON.parse(readFileSync(new URL(relativePath, import.meta.url), 'utf8'));
}

async function stageReleaseManifests({ packager }) {
const stage = await packager.info.tempDirManager.createTempDir({
prefix: 'maka-release-manifests',
});
for (const name of ['mcp', 'runtime', 'runtime-host']) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P3] The Electron staging path prunes release manifests only for mcp, runtime, and runtime-host. The npm-CLI and standalone paths prune every workspace, so the Electron asar keeps whatever exports each other @maka/* package ships as-is, including any ./test-only/* branches. If the desktop depends on a package other than these three that exposes test-only exports, they will ride into the archive. Consider deriving the full set of shipped @maka package names (e.g. from the desktop's dependencies) instead of a hardcoded triple, so staging stays consistent with the other two packaging paths.

const directory = join(stage, name);
mkdirSync(directory, { recursive: true });
writeFileSync(
join(directory, 'package.json'),
`${JSON.stringify(workspaceReleaseManifest(readManifest(`../../packages/${name}/package.json`)), null, 2)}\n`,
);
}
packager.config.files.push({ from: stage, to: 'node_modules/@maka' });
}

const rootManifest = readManifest('../../package.json');
const { runtimeHostSetupPackage } = resolveProductManifestIdentity({
rootManifest,
Expand All @@ -41,6 +58,7 @@ const baseDesktopBuilderConfig = {
productName: 'Maka',
artifactName: 'Maka-${version}-mac-${arch}.${ext}',
asar: true,
beforePack: stageReleaseManifests,
extraMetadata: { runtimeHostSetupPackage, makaUpdateChannel: 'release' },
directories: {
output: 'release',
Expand All @@ -58,6 +76,7 @@ const baseDesktopBuilderConfig = {
'dist/**/*',
'dist-renderer/**/*',
'package.json',
'!node_modules/@maka/{mcp,runtime,runtime-host}/package.json',
'!**/__tests__/**',
// FakeBackend and the Desktop E2E candidate bootstrap live under
// `test-only/`; they must not reach a packaged app.
Expand Down
23 changes: 9 additions & 14 deletions scripts/package-macos-arm64-cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
releaseNpmEnvironment,
resolveReleaseWorkspacePackages,
resolveWorkspaceReleaseFiles,
workspaceReleaseManifest,
} from './release-cli-file-policy.mjs';

const execFileAsync = promisify(execFile);
Expand Down Expand Up @@ -205,31 +206,25 @@ export async function stageWorkspacePackages(installRoot, workspacePackages) {
...workspacePackages.map(async ({ directory, manifest, workspacePath }) => {
const targetDirectory = join(installRoot, workspacePath);
await mkdir(targetDirectory, { recursive: true });
await copyFile(join(directory, 'package.json'), join(targetDirectory, 'package.json'));
await writeFile(
join(targetDirectory, 'package.json'),
`${JSON.stringify(workspaceReleaseManifest(manifest), null, 2)}\n`,
);
await Promise.all(
resolveWorkspaceReleaseFiles(directory, manifest).map(async (releaseFile) => {
const source = join(directory, ...releaseFile.split('/'));
const target = join(targetDirectory, ...releaseFile.split('/'));
await mkdir(dirname(target), { recursive: true });
await cp(source, target, { recursive: true });
await cp(source, target, {
recursive: true,
filter: (path) => !isMakaDevelopmentArtifact(relative(directory, path)),
});
}),
);
await pruneMakaDevelopmentArtifacts(targetDirectory);
}),
]);
}

async function pruneMakaDevelopmentArtifacts(directory, root = directory) {
for (const entry of await readdir(directory, { withFileTypes: true })) {
const path = join(directory, entry.name);
if (isMakaDevelopmentArtifact(relative(root, path))) {
await rm(path, { recursive: entry.isDirectory(), force: true });
} else if (entry.isDirectory()) {
await pruneMakaDevelopmentArtifacts(path, root);
}
}
}

export async function listDependencyPatchNames() {
let entries;
try {
Expand Down
44 changes: 41 additions & 3 deletions scripts/product-release.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,32 @@ test('Desktop packaging derives the Runtime Host setup package from product mani
]);
});

test('Desktop stages release manifests before electron-builder builds the archive', async (t) => {
const stage = await mkdtemp(join(tmpdir(), 'maka-desktop-release-manifests-'));
t.after(() => rm(stage, { recursive: true, force: true }));
const files = [{ filter: [...desktopBuilderConfig.files] }];

await desktopBuilderConfig.beforePack({
packager: {
config: { files },
info: { tempDirManager: { createTempDir: async () => stage } },
},
});

assert.equal(files.length, 2);
assert.ok(
files[0].filter.includes('!node_modules/@maka/{mcp,runtime,runtime-host}/package.json'),
);
assert.deepEqual(files.at(-1), { from: stage, to: 'node_modules/@maka' });
for (const name of ['mcp', 'runtime', 'runtime-host']) {
const projected = JSON.parse(await readFile(join(stage, name, 'package.json')));
assert.equal(
Object.keys(projected.exports).some((subpath) => subpath.startsWith('./test-only/')),
false,
);
}
});

test('Desktop packaging does not distribute the retired bundled Git runtime', () => {
const resources = desktopBuilderConfig.extraResources.map(({ from, to }) => ({ from, to }));
assert.equal(
Expand Down Expand Up @@ -554,14 +580,22 @@ test('standalone packaging applies the shared CLI file policy to dependencies',
}
});

test('standalone workspace staging keeps runtime files and removes Maka development output', async () => {
test('standalone workspace staging keeps manifests consistent with copied files', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-standalone-workspace-'));
const workspace = join(root, 'workspace');
const install = join(root, 'install');
try {
const manifest = {
name: '@maka/example',
exports: {
'.': './dist/index.js',
'./test-only/fixture': './dist/__tests__/fixture.js',
},
scripts: { build: 'tsc' },
};
await mkdir(join(workspace, 'dist', '__tests__'), { recursive: true });
await mkdir(install);
await writeFile(join(workspace, 'package.json'), '{}\n');
await writeFile(join(workspace, 'package.json'), `${JSON.stringify(manifest)}\n`);
await writeFile(join(workspace, 'dist', 'index.js'), 'runtime\n');
await writeFile(join(workspace, 'dist', 'dev-cli.js'), 'development\n');
await writeFile(join(workspace, 'dist', 'index.d.ts'), 'development\n');
Expand All @@ -571,11 +605,15 @@ test('standalone workspace staging keeps runtime files and removes Maka developm
await stageWorkspacePackages(install, [
{
directory: workspace,
manifest: { name: '@maka/example' },
manifest,
workspacePath: 'packages/example',
},
]);

assert.deepEqual(JSON.parse(await readFile(join(install, 'packages/example/package.json'))), {
name: '@maka/example',
exports: { '.': './dist/index.js' },
});
const staged = join(install, 'packages', 'example', 'dist');
assert.equal(await readFile(join(staged, 'index.js'), 'utf8'), 'runtime\n');
for (const path of ['dev-cli.js', 'index.d.ts', 'index.js.map', '__tests__/fixture.js']) {
Expand Down
49 changes: 49 additions & 0 deletions scripts/release-cli-file-policy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,55 @@ export function isMakaDevelopmentArtifact(relativePath) {
);
}

function releaseExportTarget(target) {
if (typeof target === 'string') {
return isMakaDevelopmentArtifact(target) ? undefined : target;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P3] Leaf-level pruning also drops *.d.ts(map) and *.js.map targets from the release exports and strips source maps from copied dist. For CLI/internal packages that don't ship types this is intended, but the reachability consequence is worth an explicit note: production stack traces will no longer be source-mapped, and any consumer expecting types from @maka/* will not find them in the release. I verified no shipped (non-test) module imports a pruned branch (only e2e/test-only code references test-only/*), so this is not a correctness break today — just record the intended surface.

}
if (Array.isArray(target)) {
const projected = target.map(releaseExportTarget).filter((entry) => entry !== undefined);
return projected.length === 0 ? undefined : projected;
}
if (target && typeof target === 'object') {
const projected = Object.fromEntries(
Object.entries(target)
.map(([condition, value]) => [condition, releaseExportTarget(value)])
.filter(([, value]) => value !== undefined),
);
return Object.keys(projected).length === 0 ? undefined : projected;
}
return target;
}

const WORKSPACE_RELEASE_MANIFEST_FIELDS = [
'name',
'version',
'description',
'license',
'type',
'sideEffects',
'main',
'exports',
'bin',
'engines',
'dependencies',
'optionalDependencies',
'peerDependencies',
'peerDependenciesMeta',
];

export function workspaceReleaseManifest(manifest) {
const releaseManifest = Object.fromEntries(
WORKSPACE_RELEASE_MANIFEST_FIELDS.filter((field) => manifest[field] !== undefined).map(
(field) => [field, manifest[field]],
),
);
if (releaseManifest.exports !== undefined) {
const projected = releaseExportTarget(releaseManifest.exports);
releaseManifest.exports = projected === undefined ? {} : projected;
}
return releaseManifest;
}

export function isCurrentDevelopmentJavaScript(
workspaceRoot,
relativePath,
Expand Down
57 changes: 57 additions & 0 deletions scripts/release-cli-file-policy.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
orderWorkspaceBuilds,
renderNpmReadme,
resolveWorkspaceReleaseFiles,
workspaceReleaseManifest,
workspaceReleaseFiles,
} from './release-cli-file-policy.mjs';

Expand All @@ -51,6 +52,62 @@ test('the npm README receives the canonical WIP disclaimer exactly once', () =>
});

describe('CLI release file policy', () => {
test('release manifests omit exports whose targets are excluded from Maka artifacts', () => {
const repoRoot = resolve(import.meta.dirname, '..');
for (const [directory, omitted] of Object.entries({
mcp: ['./test-only/stdio-server'],
'runtime-host': [
'./test-only/client-capability-host',
'./test-only/execution-candidate-e2e-main',
],
runtime: ['./test-only/fake-backend', './test-only/observation-text-reader'],
})) {
const manifestPath = join(repoRoot, 'packages', directory, 'package.json');
const source = JSON.parse(readFileSync(manifestPath, 'utf8'));
const projected = workspaceReleaseManifest(source);

assert.deepEqual(
Object.keys(source.exports).filter((subpath) => !Object.hasOwn(projected.exports, subpath)),
omitted,
);
assert.deepEqual(JSON.parse(readFileSync(manifestPath, 'utf8')), source);
}
});

test('release manifests project conditional exports leaf by leaf', () => {
const source = {
exports: {
'.': {
types: './dist/index.d.ts',
default: './dist/index.js',
},
'./fallback': [
'./dist/__tests__/fixture.js',
{ types: './dist/fallback.d.ts', default: './dist/fallback.js' },
],
},
};
const original = structuredClone(source);

assert.deepEqual(workspaceReleaseManifest(source).exports, {
'.': { default: './dist/index.js' },
'./fallback': [{ default: './dist/fallback.js' }],
});
assert.deepEqual(source, original);
});

test('release manifests preserve a top-level export target array', () => {
assert.deepEqual(
workspaceReleaseManifest({
exports: [
'./dist/__tests__/fixture.js',
{ types: './dist/index.d.ts', default: './dist/index.js' },
],
}).exports,
[{ default: './dist/index.js' }],
);
});

test('derives the runtime workspace build order from production dependencies', () => {
const manifests = new Map([
['maka-agent', { name: 'maka-agent', dependencies: { '@maka/eval': '0.1.0' } }],
Expand Down
23 changes: 2 additions & 21 deletions scripts/release-cli-package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
releaseNpmEnvironment,
resolveReleaseWorkspacePackages,
resolveWorkspaceReleaseFiles,
workspaceReleaseManifest,
} from './release-cli-file-policy.mjs';

const repoRoot = resolve(import.meta.dirname, '..');
Expand Down Expand Up @@ -425,27 +426,7 @@ function dependencyDestination(dependency) {
function copyInternalPackage(source, destination) {
mkdirSync(destination, { recursive: true, mode: 0o755 });
const manifest = readJson(join(source, 'package.json'));
const allowedFields = [
'name',
'version',
'description',
'license',
'type',
'sideEffects',
'main',
'exports',
'bin',
'engines',
'dependencies',
'optionalDependencies',
'peerDependencies',
'peerDependenciesMeta',
];
const releaseManifest = Object.fromEntries(
allowedFields
.filter((field) => manifest[field] !== undefined)
.map((field) => [field, manifest[field]]),
);
const releaseManifest = workspaceReleaseManifest(manifest);
writeFileSync(join(destination, 'package.json'), `${JSON.stringify(releaseManifest, null, 2)}\n`);
for (const releaseFile of resolveWorkspaceReleaseFiles(source, manifest)) {
if (releaseFile === 'dist') copyRuntimeDist(source, destination, manifest.name);
Expand Down
Loading