import fs from 'node:fs'; import path from 'node:path'; export const GENERATED_ROOT = 'test/qkforce'; export const GENERATED_MANIFEST = `${GENERATED_ROOT}/manifest.json`; const fail = message => { throw new Error(`Invalid QKForce generated workspace: ${message}`); }; function localFile(root, relative) { if (typeof relative !== 'string' || !relative.startsWith(`${GENERATED_ROOT}/`) || relative.includes('\\') || relative.split('/').some(part => !part || part === '.' || part === '..')) { fail('unsafe generated file path'); } let current = path.resolve(root); for (const segment of relative.split('/')) { current = path.join(current, segment); let stat; try { stat = fs.lstatSync(current); } catch { fail('declared generated file is missing'); } if (stat.isSymbolicLink()) fail('generated paths must not contain symlinks'); } if (!fs.statSync(current).isFile()) fail('declared generated input must be a file'); return current; } export function loadGeneratedWorkspace(root = process.cwd()) { const manifestPath = path.join(root, GENERATED_MANIFEST); if (!fs.existsSync(manifestPath)) { // A local template checkout may run the example. An API package never may. if (fs.existsSync(path.join(root, 'qkforce-package-manifest.json'))) fail('regenerate the project before execution'); return null; } let manifest; try { manifest = JSON.parse(fs.readFileSync(localFile(root, GENERATED_MANIFEST), 'utf8')); } catch { fail('generated manifest is not readable JSON'); } if (manifest?.schemaVersion !== 1 || !Array.isArray(manifest.features) || manifest.features.length === 0) fail('select at least one project test case'); if (new Set(manifest.features).size !== manifest.features.length) fail('duplicate feature paths'); for (const file of manifest.features) { if (typeof file !== 'string' || !file.startsWith(`${GENERATED_ROOT}/features/`) || !file.endsWith('.feature')) fail('invalid feature path'); localFile(root, file); } if (manifest.steps !== `${GENERATED_ROOT}/steps.mjs` || manifest.selectors !== `${GENERATED_ROOT}/selectors.json`) fail('unsupported generated module paths'); localFile(root, manifest.steps); localFile(root, manifest.selectors); return Object.freeze({ specs: manifest.features.map(file => `./${file}`), imports: [`./${manifest.steps}`] }); } export function loadGeneratedSelectors(root = process.cwd()) { const value = JSON.parse(fs.readFileSync(localFile(root, `${GENERATED_ROOT}/selectors.json`), 'utf8')); if (!value || typeof value !== 'object' || Array.isArray(value)) fail('selectors must be an object'); return value; }