150 lines
4.7 KiB
JavaScript
150 lines
4.7 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { access, mkdir, readFile, rm, symlink, writeFile } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
|
|
import { buildWeb } from './build-web.mjs';
|
|
|
|
const fixtureRoot = path.join(process.cwd(), '.mosaic-test-work', `build-web-${process.pid}`);
|
|
|
|
async function fixture(name) {
|
|
const root = path.join(fixtureRoot, name);
|
|
await mkdir(path.join(root, 'apps', 'web', '.next'), { recursive: true });
|
|
return root;
|
|
}
|
|
|
|
async function exists(target) {
|
|
try {
|
|
await access(target);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
test.after(async () => {
|
|
await rm(fixtureRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
test('a successful web build atomically publishes its source and symlink certification', async () => {
|
|
const root = await fixture('success');
|
|
const marker = path.join(root, 'apps', 'web', '.next', '.mosaic-source-hash');
|
|
const manifest = path.join(root, 'apps', 'web', '.next', '.mosaic-symlink-manifest');
|
|
|
|
await buildWeb({ root, fingerprint: async () => 'certified', runBuild: async () => {} });
|
|
|
|
assert.deepEqual(JSON.parse(await readFile(marker, 'utf8')), {
|
|
version: 1,
|
|
sourceFingerprint: 'certified',
|
|
symlinkManifestHash: '8a5a375cea6a55d24bd5f875856da63feba33adbefb15a92a0007719b84bcf11',
|
|
});
|
|
assert.equal(await readFile(manifest, 'utf8'), '{"version":1,"links":[]}\n');
|
|
});
|
|
|
|
test('a failed web build leaves no certification marker', async () => {
|
|
const root = await fixture('failure');
|
|
const marker = path.join(root, 'apps', 'web', '.next', '.mosaic-source-hash');
|
|
const manifest = path.join(root, 'apps', 'web', '.next', '.mosaic-symlink-manifest');
|
|
await writeFile(marker, 'stale\n');
|
|
await writeFile(manifest, 'stale\n');
|
|
|
|
await assert.rejects(
|
|
buildWeb({
|
|
root,
|
|
fingerprint: async () => 'before',
|
|
runBuild: async () => {
|
|
throw new Error('build failed');
|
|
},
|
|
}),
|
|
/build failed/,
|
|
);
|
|
|
|
assert.equal(await exists(marker), false);
|
|
assert.equal(await exists(manifest), false);
|
|
});
|
|
|
|
test('overlapping web builds are serialized while the marker remains absent', async () => {
|
|
const root = await fixture('overlap');
|
|
const marker = path.join(root, 'apps', 'web', '.next', '.mosaic-source-hash');
|
|
const manifest = path.join(root, 'apps', 'web', '.next', '.mosaic-symlink-manifest');
|
|
await writeFile(marker, 'stale\n');
|
|
await writeFile(manifest, 'stale\n');
|
|
let releaseFirst;
|
|
let secondEntered = false;
|
|
const firstEntered = new Promise((resolve) => {
|
|
releaseFirst = resolve;
|
|
});
|
|
let markFirstEntered;
|
|
const firstStarted = new Promise((resolve) => {
|
|
markFirstEntered = resolve;
|
|
});
|
|
|
|
const first = buildWeb({
|
|
root,
|
|
fingerprint: async () => 'certified',
|
|
runBuild: async () => {
|
|
markFirstEntered();
|
|
await firstEntered;
|
|
},
|
|
});
|
|
await firstStarted;
|
|
const second = buildWeb({
|
|
root,
|
|
fingerprint: async () => 'certified',
|
|
runBuild: async () => {
|
|
secondEntered = true;
|
|
},
|
|
});
|
|
await new Promise((resolve) => setTimeout(resolve, 75));
|
|
assert.equal(secondEntered, false);
|
|
assert.equal(await exists(marker), false);
|
|
assert.equal(await exists(manifest), false);
|
|
|
|
releaseFirst();
|
|
await Promise.all([first, second]);
|
|
assert.equal(secondEntered, true);
|
|
assert.equal(JSON.parse(await readFile(marker, 'utf8')).sourceFingerprint, 'certified');
|
|
assert.equal(await readFile(manifest, 'utf8'), '{"version":1,"links":[]}\n');
|
|
});
|
|
|
|
test('a build that replaces .next with a symbolic link cannot publish outside the checkout', async () => {
|
|
const root = await fixture('symbolic-next');
|
|
const nextDir = path.join(root, 'apps', 'web', '.next');
|
|
const outside = path.join(root, 'outside-generated');
|
|
await mkdir(outside);
|
|
|
|
await assert.rejects(
|
|
buildWeb({
|
|
root,
|
|
fingerprint: async () => 'certified',
|
|
runBuild: async () => {
|
|
await rm(nextDir, { recursive: true });
|
|
await symlink(outside, nextDir);
|
|
},
|
|
}),
|
|
/must be a real directory/,
|
|
);
|
|
|
|
assert.equal(await exists(path.join(outside, '.mosaic-source-hash')), false);
|
|
assert.equal(await exists(path.join(outside, '.mosaic-symlink-manifest')), false);
|
|
});
|
|
|
|
test('inputs changed during a web build are not certified', async () => {
|
|
const root = await fixture('changed-inputs');
|
|
const marker = path.join(root, 'apps', 'web', '.next', '.mosaic-source-hash');
|
|
const manifest = path.join(root, 'apps', 'web', '.next', '.mosaic-symlink-manifest');
|
|
const fingerprints = ['before', 'after'];
|
|
|
|
await assert.rejects(
|
|
buildWeb({
|
|
root,
|
|
fingerprint: async () => fingerprints.shift(),
|
|
runBuild: async () => {},
|
|
}),
|
|
/inputs changed during next build/,
|
|
);
|
|
|
|
assert.equal(await exists(marker), false);
|
|
assert.equal(await exists(manifest), false);
|
|
});
|