在 Makelore 构建并预检静态发布产物

This commit is contained in:
2026-08-12 21:19:39 +08:00
parent 3a80625fe2
commit 5b44864265
26 changed files with 1275 additions and 217 deletions

View File

@@ -0,0 +1,71 @@
import { createHash } from 'node:crypto';
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { createProjectConfig } from '../../shared/project-config';
const runElectronNodeMock = vi.hoisted(() => vi.fn());
vi.mock('@electron/services/publish-runtime', () => ({
resolvePublishRuntime: async () => ({ nodeExecutable: 'electron', npmCli: 'npm-cli.js', nodeVersion: '22.0.0', npmVersion: '11.6.2' }),
runElectronNode: runElectronNodeMock,
PublishRuntimeError: class extends Error {},
}));
import { prepareProjectRelease } from '@electron/services/project-release-builder';
import { startStaticReleaseServer, staticArtifactSnapshotFiles } from '@electron/services/static-release-server';
describe('project release builder', () => {
let root: string | null = null;
afterEach(async () => { if (root) await rm(root, { recursive: true, force: true }); root = null; vi.clearAllMocks(); });
it('builds from the exact source archive and returns the protocol-v1 canonical contract', async () => {
root = await mkdtemp(join(tmpdir(), 'release-builder-test-'));
await mkdir(join(root, '.niancode'), { recursive: true });
await writeFile(join(root, '.niancode', 'project.json'), JSON.stringify(createProjectConfig(new Date().toISOString(), 'mini_game')));
await writeFile(join(root, 'package.json'), JSON.stringify({ name: 'demo', packageManager: 'npm@11.6.2', devDependencies: { vite: '7.3.1' } }));
await writeFile(join(root, 'package-lock.json'), JSON.stringify({ name: 'demo', lockfileVersion: 3, requires: true, packages: {} }));
await writeFile(join(root, 'index.html'), '<main>source</main>');
runElectronNodeMock.mockImplementation(async (input: { args: string[]; cwd: string }) => {
if (input.args.includes('ci')) {
await mkdir(join(input.cwd, 'node_modules', 'vite', 'bin'), { recursive: true });
await writeFile(join(input.cwd, 'node_modules', 'vite', 'package.json'), JSON.stringify({ version: '7.3.1' }));
} else if (input.args.includes('build')) {
const output = input.args[input.args.indexOf('--outDir') + 1];
await mkdir(join(output, 'assets'), { recursive: true });
await writeFile(join(output, 'index.html'), '<main>built</main>');
await writeFile(join(output, 'assets', '中.js'), 'ok');
}
return { stdout: '', stderr: '' };
});
const release = await prepareProjectRelease({ projectPath: root, clientVersion: '2.0.0' });
try {
expect(release.contract).toMatchObject({
schema_version: 1, entry_path: 'index.html', security_profile: 'works-square-static-sandbox-v1',
toolchain: { client: 'makelore', client_version: '2.0.0', node: '22.0.0', npm: '11.6.2', vite: '7.3.1' },
});
expect(release.contract.files.map((file) => file.path)).toEqual(['assets/中.js', 'index.html']);
const canonical = JSON.stringify(release.contract.files.map(({ path, sha256, size }) => ({ path, sha256, size })));
expect(release.contract.artifact_digest).toBe(createHash('sha256').update(canonical).digest('hex'));
expect(release.contract.source_digest).toBe(createHash('sha256').update(await readFile(release.sourceArchive.path)).digest('hex'));
expect(release.contract.built_archive_digest).toBe(createHash('sha256').update(release.builtArchive.bytes).digest('hex'));
expect(runElectronNodeMock.mock.calls[0][0].args).toEqual([
'npm-cli.js', 'ci', '--ignore-scripts', '--no-audit', '--no-fund',
'--userconfig', expect.stringMatching(/empty-npmrc$/), '--cache', expect.stringMatching(/npm-cache$/),
]);
await rm(release.distRoot, { recursive: true, force: true });
expect(staticArtifactSnapshotFiles(release.staticArtifact).map(({ path, bytes }) => ({
path,
size: bytes.length,
sha256: createHash('sha256').update(bytes).digest('hex'),
}))).toEqual(release.contract.files);
const server = await startStaticReleaseServer(release.staticArtifact);
try {
expect(await (await fetch(server.entryUrl)).text()).toBe('<main>built</main>');
expect(await (await fetch(new URL('assets/%E4%B8%AD.js', server.entryUrl))).text()).toBe('ok');
} finally {
await server.close();
}
} finally { await release.dispose(); }
await expect(readFile(release.sourceArchive.path)).rejects.toThrow();
});
});