fix: make checkout gates environment-aware
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { access, mkdir, rename, rm } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const root = process.cwd();
|
||||
const generated = path.join(root, 'apps', 'web', '.next');
|
||||
const quarantineRoot = path.join(root, '.mosaic-test-work', 'generated-quarantine');
|
||||
|
||||
try {
|
||||
await access(generated);
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') process.exit(0);
|
||||
throw error;
|
||||
}
|
||||
|
||||
await mkdir(quarantineRoot, { recursive: true });
|
||||
const quarantine = path.join(quarantineRoot, `web-next-${Date.now()}-${process.pid}`);
|
||||
try {
|
||||
await rename(generated, quarantine);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`MOSAIC_GENERATED_CLEAN_FAILED: could not quarantine apps/web/.next. Fix: sudo rm -rf '${generated}', then rerun pnpm preflight`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
await rm(quarantine, { recursive: true, force: true });
|
||||
} catch {
|
||||
console.warn(
|
||||
`Generated state was deactivated but could not be deleted; quarantined at ${quarantine}`,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { access, lstat, mkdir, readFile, readdir, rename, rm } from 'node:fs/promises';
|
||||
import { execFile, spawn } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import path from 'node:path';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
function run(command, args, options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, options);
|
||||
child.once('error', reject);
|
||||
child.once('exit', (code, signal) => {
|
||||
if (code === 0) resolve();
|
||||
else reject(new Error(signal ? `husky terminated by ${signal}` : `husky exited ${code}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function pathExists(target) {
|
||||
try {
|
||||
await access(target);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') return false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function directorySnapshot(root) {
|
||||
const snapshot = [];
|
||||
async function walk(current) {
|
||||
const children = await readdir(current, { withFileTypes: true });
|
||||
for (const child of children.sort((left, right) => left.name.localeCompare(right.name))) {
|
||||
const target = path.join(current, child.name);
|
||||
const relative = path.relative(root, target);
|
||||
const stats = await lstat(target);
|
||||
if (child.isDirectory()) {
|
||||
snapshot.push([relative, 'directory', stats.mode & 0o777]);
|
||||
await walk(target);
|
||||
} else {
|
||||
snapshot.push([
|
||||
relative,
|
||||
'file',
|
||||
stats.mode & 0o777,
|
||||
(await readFile(target)).toString('base64'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
await walk(root);
|
||||
return JSON.stringify(snapshot);
|
||||
}
|
||||
|
||||
async function directoriesMatch(left, right) {
|
||||
return (await directorySnapshot(left)) === (await directorySnapshot(right));
|
||||
}
|
||||
|
||||
export async function installHooks({
|
||||
root = process.cwd(),
|
||||
disabled = process.env.HUSKY === '0',
|
||||
quarantineRoot = path.join(root, '.mosaic-test-work', 'husky-quarantine'),
|
||||
runHusky = async (_stagingHooks, stagingRepo) => {
|
||||
await execFileAsync('git', ['init', '--quiet', stagingRepo]);
|
||||
await run(path.join(root, 'node_modules', '.bin', 'husky'), ['.husky'], {
|
||||
cwd: stagingRepo,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
},
|
||||
activateHooks = async () => {
|
||||
await run('git', ['config', 'core.hooksPath', '.husky/_'], { cwd: root, stdio: 'inherit' });
|
||||
},
|
||||
readHooksPath = async () => {
|
||||
try {
|
||||
const { stdout } = await execFileAsync('git', ['config', '--get', 'core.hooksPath'], {
|
||||
cwd: root,
|
||||
});
|
||||
return stdout.trim() || null;
|
||||
} catch (error) {
|
||||
if (error.code === 1) return null;
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
restoreHooksPath = async (previous) => {
|
||||
if (previous === null) {
|
||||
try {
|
||||
await execFileAsync('git', ['config', '--unset', 'core.hooksPath'], { cwd: root });
|
||||
} catch (error) {
|
||||
if (error.code !== 5) throw error;
|
||||
}
|
||||
} else {
|
||||
await execFileAsync('git', ['config', 'core.hooksPath', previous], { cwd: root });
|
||||
}
|
||||
},
|
||||
} = {}) {
|
||||
if (disabled) return;
|
||||
|
||||
const huskyDir = path.join(root, '.husky');
|
||||
const active = path.join(huskyDir, '_');
|
||||
const nonce = `${Date.now()}-${process.pid}`;
|
||||
const stagingRepo = path.join(root, '.mosaic-test-work', `husky-stage-${nonce}`);
|
||||
const stagingHooks = path.join(stagingRepo, '.husky');
|
||||
const quarantined = path.join(quarantineRoot, `${path.basename(root)}-${nonce}`);
|
||||
|
||||
const previousHooksPath = await readHooksPath();
|
||||
await mkdir(huskyDir, { recursive: true });
|
||||
await mkdir(quarantineRoot, { recursive: true });
|
||||
|
||||
const previousComplete = (await pathExists(active)) && (await pathExists(path.join(active, 'h')));
|
||||
let previousQuarantined = false;
|
||||
try {
|
||||
if ((await pathExists(active)) && !previousComplete) {
|
||||
await rename(active, quarantined);
|
||||
previousQuarantined = true;
|
||||
}
|
||||
await mkdir(stagingRepo, { recursive: true });
|
||||
await runHusky(stagingHooks, stagingRepo);
|
||||
const staged = path.join(stagingHooks, '_');
|
||||
if (!(await pathExists(path.join(staged, 'h')))) {
|
||||
throw new Error('husky did not produce its required h shim');
|
||||
}
|
||||
if (previousComplete) {
|
||||
if (!(await directoriesMatch(active, staged))) {
|
||||
throw new Error('existing complete hook set differs from the installed Husky version');
|
||||
}
|
||||
await rm(stagingRepo, { recursive: true, force: true });
|
||||
} else {
|
||||
await rename(staged, active);
|
||||
await rm(stagingRepo, { recursive: true, force: true });
|
||||
}
|
||||
await activateHooks();
|
||||
if (previousQuarantined) {
|
||||
try {
|
||||
await rm(quarantined, { recursive: true, force: true });
|
||||
} catch {
|
||||
console.warn(
|
||||
`Previous hook state was deactivated but remains quarantined at ${quarantined}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const cleanupFailures = [];
|
||||
try {
|
||||
if ((await pathExists(active)) && !previousComplete) {
|
||||
await rename(active, `${quarantined}-failed`);
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
cleanupFailures.push(`active hooks: ${cleanupError.message}`);
|
||||
}
|
||||
try {
|
||||
if (await pathExists(stagingRepo)) {
|
||||
await rename(stagingRepo, `${quarantined}-staging`);
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
cleanupFailures.push(`staging hooks: ${cleanupError.message}`);
|
||||
}
|
||||
try {
|
||||
await restoreHooksPath(previousHooksPath);
|
||||
} catch (cleanupError) {
|
||||
cleanupFailures.push(`core.hooksPath: ${cleanupError.message}`);
|
||||
}
|
||||
const cleanup =
|
||||
cleanupFailures.length === 0
|
||||
? 'No partial hook set was activated.'
|
||||
: `Automatic cleanup was incomplete (${cleanupFailures.join('; ')}).`;
|
||||
throw new Error(
|
||||
`Hook installation failed: ${error.message}. ${cleanup} Fix: rm -rf .husky/_ && git config core.hooksPath .husky/_ && pnpm install --frozen-lockfile`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
try {
|
||||
await installHooks();
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { access, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
import { installHooks } from './install-hooks.mjs';
|
||||
|
||||
const fixtureRoot = path.join(process.cwd(), '.mosaic-test-work', `hooks-${process.pid}`);
|
||||
const quarantineRoot = path.join(fixtureRoot, 'quarantine');
|
||||
|
||||
async function fixture(name) {
|
||||
const root = path.join(fixtureRoot, name);
|
||||
await mkdir(path.join(root, '.husky'), { 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('an interrupted install quarantines the partial active hook set and fails loudly', async () => {
|
||||
const root = await fixture('interrupted');
|
||||
let restoredHooksPath = 'not-called';
|
||||
|
||||
await assert.rejects(
|
||||
installHooks({
|
||||
root,
|
||||
quarantineRoot,
|
||||
runHusky: async (stagingHooks) => {
|
||||
await mkdir(path.join(stagingHooks, '_'), { recursive: true });
|
||||
await writeFile(path.join(stagingHooks, '_', 'h'), 'partial');
|
||||
throw new Error('simulated interruption');
|
||||
},
|
||||
activateHooks: async () => {},
|
||||
readHooksPath: async () => null,
|
||||
restoreHooksPath: async (value) => {
|
||||
restoredHooksPath = value;
|
||||
},
|
||||
}),
|
||||
(error) => {
|
||||
assert.match(error.message, /Hook installation failed/);
|
||||
assert.match(error.message, /pnpm install --frozen-lockfile/);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(await exists(path.join(root, '.husky', '_')), false);
|
||||
assert.equal(restoredHooksPath, null);
|
||||
const quarantined = await readdir(quarantineRoot);
|
||||
assert.equal(quarantined.length, 1);
|
||||
});
|
||||
|
||||
test('a failed replacement restores a previously complete active hook set', async () => {
|
||||
const root = await fixture('rollback');
|
||||
const activeShim = path.join(root, '.husky', '_', 'h');
|
||||
await mkdir(path.dirname(activeShim), { recursive: true });
|
||||
await writeFile(activeShim, 'previous-complete');
|
||||
let previousRemainedActiveDuringStaging = false;
|
||||
|
||||
await assert.rejects(
|
||||
installHooks({
|
||||
root,
|
||||
quarantineRoot: path.join(fixtureRoot, 'rollback-quarantine'),
|
||||
runHusky: async () => {
|
||||
previousRemainedActiveDuringStaging =
|
||||
(await readFile(activeShim, 'utf8')) === 'previous-complete';
|
||||
throw new Error('simulated replacement failure');
|
||||
},
|
||||
activateHooks: async () => {},
|
||||
readHooksPath: async () => '.husky/_',
|
||||
restoreHooksPath: async () => {},
|
||||
}),
|
||||
/Hook installation failed/,
|
||||
);
|
||||
|
||||
assert.equal(previousRemainedActiveDuringStaging, true);
|
||||
assert.equal(await readFile(activeShim, 'utf8'), 'previous-complete');
|
||||
});
|
||||
|
||||
test('a mismatched complete hook set fails loudly instead of reporting a stale install as current', async () => {
|
||||
const root = await fixture('mismatch');
|
||||
const activeShim = path.join(root, '.husky', '_', 'h');
|
||||
await mkdir(path.dirname(activeShim), { recursive: true });
|
||||
await writeFile(activeShim, 'old-complete');
|
||||
|
||||
await assert.rejects(
|
||||
installHooks({
|
||||
root,
|
||||
quarantineRoot: path.join(fixtureRoot, 'mismatch-quarantine'),
|
||||
runHusky: async (stagingHooks) => {
|
||||
await mkdir(path.join(stagingHooks, '_'), { recursive: true });
|
||||
await writeFile(path.join(stagingHooks, '_', 'h'), 'new-complete');
|
||||
},
|
||||
activateHooks: async () => {},
|
||||
readHooksPath: async () => '.husky/_',
|
||||
restoreHooksPath: async () => {},
|
||||
}),
|
||||
/Hook installation failed.*pnpm install --frozen-lockfile/,
|
||||
);
|
||||
|
||||
assert.equal(await readFile(activeShim, 'utf8'), 'old-complete');
|
||||
});
|
||||
|
||||
test('an explicit interactive HUSKY=0 opt-out preserves existing hooks without running installer', async () => {
|
||||
const root = await fixture('disabled');
|
||||
const activeShim = path.join(root, '.husky', '_', 'h');
|
||||
await mkdir(path.dirname(activeShim), { recursive: true });
|
||||
await writeFile(activeShim, 'preserved');
|
||||
let ran = false;
|
||||
|
||||
await installHooks({
|
||||
root,
|
||||
disabled: true,
|
||||
runHusky: async () => {
|
||||
ran = true;
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(ran, false);
|
||||
assert.equal(await readFile(activeShim, 'utf8'), 'preserved');
|
||||
});
|
||||
|
||||
test('a successful install leaves a complete active hook set', async () => {
|
||||
const root = await fixture('success');
|
||||
|
||||
await installHooks({
|
||||
root,
|
||||
quarantineRoot,
|
||||
runHusky: async (stagingHooks) => {
|
||||
await mkdir(path.join(stagingHooks, '_'), { recursive: true });
|
||||
await writeFile(path.join(stagingHooks, '_', 'h'), 'complete');
|
||||
},
|
||||
activateHooks: async () => {},
|
||||
readHooksPath: async () => null,
|
||||
restoreHooksPath: async () => {},
|
||||
});
|
||||
|
||||
assert.equal(await exists(path.join(root, '.husky', '_', 'h')), true);
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { constants } from 'node:fs';
|
||||
import { access, lstat, readdir } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import path from 'node:path';
|
||||
|
||||
export const MISSING_DEPS_EXIT = 42;
|
||||
export const GENERATED_STATE_EXIT = 43;
|
||||
|
||||
async function entries(root) {
|
||||
const result = [];
|
||||
async function walk(current) {
|
||||
let children;
|
||||
try {
|
||||
children = await readdir(current, { withFileTypes: true });
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') return;
|
||||
throw error;
|
||||
}
|
||||
for (const child of children) {
|
||||
const target = path.join(current, child.name);
|
||||
result.push(target);
|
||||
if (child.isDirectory() && !child.isSymbolicLink()) await walk(target);
|
||||
}
|
||||
}
|
||||
await walk(root);
|
||||
return result;
|
||||
}
|
||||
|
||||
async function fileMtimeRange(paths) {
|
||||
let newest = 0;
|
||||
let oldest = Number.POSITIVE_INFINITY;
|
||||
for (const target of paths) {
|
||||
const stats = await lstat(target);
|
||||
if (stats.isFile()) {
|
||||
newest = Math.max(newest, stats.mtimeMs);
|
||||
oldest = Math.min(oldest, stats.mtimeMs);
|
||||
}
|
||||
}
|
||||
return { newest, oldest: Number.isFinite(oldest) ? oldest : 0 };
|
||||
}
|
||||
|
||||
export async function runPreflight({ root = process.cwd(), uid = process.getuid?.() } = {}) {
|
||||
const binDir = path.join(root, 'node_modules', '.bin');
|
||||
const requiredBinaries = ['eslint', 'husky', 'prettier', 'tsc', 'turbo', 'vitest'];
|
||||
const missingBinaries = [];
|
||||
for (const binary of requiredBinaries) {
|
||||
try {
|
||||
await access(path.join(binDir, binary), constants.X_OK);
|
||||
} catch {
|
||||
missingBinaries.push(binary);
|
||||
}
|
||||
}
|
||||
if (missingBinaries.length > 0) {
|
||||
return {
|
||||
code: MISSING_DEPS_EXIT,
|
||||
message: `MOSAIC_PREFLIGHT_MISSING_DEPS: dependency installation is missing ${missingBinaries.join(', ')}; run pnpm install --frozen-lockfile`,
|
||||
};
|
||||
}
|
||||
|
||||
const nextDir = path.join(root, 'apps', 'web', '.next');
|
||||
let generated = [];
|
||||
try {
|
||||
await lstat(nextDir);
|
||||
generated = [nextDir, ...(await entries(nextDir))];
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') throw error;
|
||||
}
|
||||
|
||||
if (generated.length > 0) {
|
||||
const foreign = [];
|
||||
if (uid !== undefined) {
|
||||
for (const target of generated) {
|
||||
if ((await lstat(target)).uid !== uid) foreign.push(path.relative(root, target));
|
||||
}
|
||||
}
|
||||
|
||||
const sourceRoots = [
|
||||
path.join(root, 'apps', 'web', 'src'),
|
||||
path.join(root, 'apps', 'web', 'next.config.ts'),
|
||||
path.join(root, 'apps', 'web', 'package.json'),
|
||||
path.join(root, 'apps', 'web', 'tsconfig.json'),
|
||||
];
|
||||
const source = [];
|
||||
for (const sourceRoot of sourceRoots) {
|
||||
try {
|
||||
const stats = await lstat(sourceRoot);
|
||||
source.push(sourceRoot);
|
||||
if (stats.isDirectory()) source.push(...(await entries(sourceRoot)));
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const generatedTypes = generated.filter((target) =>
|
||||
target.startsWith(path.join(nextDir, 'types') + path.sep),
|
||||
);
|
||||
const sourceMtime = await fileMtimeRange(source);
|
||||
const generatedMtime = await fileMtimeRange(generatedTypes);
|
||||
const stale =
|
||||
source.length > 0 && generatedTypes.length > 0 && sourceMtime.newest > generatedMtime.oldest;
|
||||
if (foreign.length > 0 || stale) {
|
||||
const reasons = [
|
||||
foreign.length > 0 ? `foreign-owned paths: ${foreign.slice(0, 3).join(', ')}` : '',
|
||||
stale ? 'generated output is older than web source/configuration' : '',
|
||||
].filter(Boolean);
|
||||
return {
|
||||
code: GENERATED_STATE_EXIT,
|
||||
message: `MOSAIC_PREFLIGHT_GENERATED_STATE: apps/web/.next is not trustworthy (${reasons.join('; ')}); run pnpm clean:generated, then rerun the gate`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { code: 0, message: 'checkout preflight passed' };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const result = await runPreflight();
|
||||
const stream = result.code === 0 ? process.stdout : process.stderr;
|
||||
stream.write(`${result.message}\n`);
|
||||
process.exitCode = result.code;
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
await main();
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { chmod, mkdir, rm, symlink, utimes, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
import { runPreflight } from './preflight.mjs';
|
||||
|
||||
const fixtureRoot = path.join(process.cwd(), '.mosaic-test-work', `preflight-${process.pid}`);
|
||||
|
||||
const requiredBins = ['eslint', 'husky', 'prettier', 'tsc', 'turbo', 'vitest'];
|
||||
|
||||
async function fixture(name) {
|
||||
const root = path.join(fixtureRoot, name);
|
||||
await mkdir(path.join(root, 'apps', 'web', 'src', 'app'), { recursive: true });
|
||||
await writeFile(path.join(root, 'apps', 'web', 'src', 'app', 'page.tsx'), 'export default 1;\n');
|
||||
return root;
|
||||
}
|
||||
|
||||
async function installRequiredBins(root) {
|
||||
const binDir = path.join(root, 'node_modules', '.bin');
|
||||
await mkdir(binDir, { recursive: true });
|
||||
await Promise.all(
|
||||
requiredBins.map(async (name) => {
|
||||
const target = path.join(binDir, name);
|
||||
await writeFile(target, '');
|
||||
await chmod(target, 0o755);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
test.after(async () => {
|
||||
await rm(fixtureRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('missing dependencies have a dedicated exit code and install remediation', async () => {
|
||||
const root = await fixture('missing-deps');
|
||||
const result = await runPreflight({ root });
|
||||
|
||||
assert.equal(result.code, 42);
|
||||
assert.match(result.message, /MOSAIC_PREFLIGHT_MISSING_DEPS/);
|
||||
assert.match(result.message, /run pnpm install/i);
|
||||
});
|
||||
|
||||
test('a partial dependency install keeps the dedicated missing-deps result', async () => {
|
||||
const root = await fixture('partial-deps');
|
||||
await mkdir(path.join(root, 'node_modules', '.bin'), { recursive: true });
|
||||
await writeFile(path.join(root, 'node_modules', '.bin', 'tsc'), '', { mode: 0o755 });
|
||||
|
||||
const result = await runPreflight({ root });
|
||||
assert.equal(result.code, 42);
|
||||
assert.match(result.message, /turbo/);
|
||||
});
|
||||
|
||||
test('a dangling required dependency shim keeps the dedicated missing-deps result', async () => {
|
||||
const root = await fixture('dangling-deps');
|
||||
await installRequiredBins(root);
|
||||
const turbo = path.join(root, 'node_modules', '.bin', 'turbo');
|
||||
await rm(turbo);
|
||||
await symlink(path.join(root, 'node_modules', 'missing-turbo'), turbo);
|
||||
|
||||
const result = await runPreflight({ root });
|
||||
assert.equal(result.code, 42);
|
||||
assert.match(result.message, /turbo/);
|
||||
});
|
||||
|
||||
test('installed dependencies pass when generated state is absent', async () => {
|
||||
const root = await fixture('clean');
|
||||
await installRequiredBins(root);
|
||||
|
||||
assert.deepEqual(await runPreflight({ root }), { code: 0, message: 'checkout preflight passed' });
|
||||
});
|
||||
|
||||
test('foreign-owned generated Next state is identified separately from source errors', async () => {
|
||||
const root = await fixture('foreign-next');
|
||||
await installRequiredBins(root);
|
||||
const generated = path.join(root, 'apps', 'web', '.next', 'types', 'validator.ts');
|
||||
await mkdir(path.dirname(generated), { recursive: true });
|
||||
await writeFile(generated, 'generated output');
|
||||
|
||||
const result = await runPreflight({ root, uid: (process.getuid?.() ?? 0) + 1 });
|
||||
assert.equal(result.code, 43);
|
||||
assert.match(result.message, /MOSAIC_PREFLIGHT_GENERATED_STATE/);
|
||||
assert.match(result.message, /foreign-owned/);
|
||||
});
|
||||
|
||||
test('stale generated Next state is identified separately from source errors', async () => {
|
||||
const root = await fixture('stale-next');
|
||||
await installRequiredBins(root);
|
||||
const generated = path.join(root, 'apps', 'web', '.next', 'types', 'validator.ts');
|
||||
await mkdir(path.dirname(generated), { recursive: true });
|
||||
await writeFile(generated, 'stale generated output');
|
||||
const old = new Date('2020-01-01T00:00:00Z');
|
||||
await utimes(generated, old, old);
|
||||
const fresh = path.join(root, 'apps', 'web', '.next', 'types', 'routes.ts');
|
||||
await writeFile(fresh, 'fresh generated output');
|
||||
const future = new Date('2030-01-01T00:00:00Z');
|
||||
await utimes(fresh, future, future);
|
||||
|
||||
const result = await runPreflight({ root });
|
||||
assert.equal(result.code, 43);
|
||||
assert.match(result.message, /MOSAIC_PREFLIGHT_GENERATED_STATE/);
|
||||
assert.match(result.message, /apps\/web\/\.next/);
|
||||
assert.match(result.message, /pnpm clean:generated/);
|
||||
});
|
||||
Reference in New Issue
Block a user