import assert from 'node:assert/strict' import { execFileSync, fork } from 'node:child_process' import { mkdtempSync, mkdirSync, writeFileSync, rmSync, renameSync } from 'node:fs' import { dirname, join, resolve } from 'node:path' import { fileURLToPath, pathToFileURL } from 'node:url' import { once } from 'node:events' const checkout = resolve(process.env.NETLIFY_VERIFY_REPO_ROOT || '/tmp/netliloop-work/build') const { callChild, getEventFromChild } = await import(pathToFileURL(join(checkout, 'packages/build/src/plugins/ipc.js')).href) const root = dirname(fileURLToPath(import.meta.url)) const cwd = mkdtempSync(join(root, 'real-plugin-repository-')) const env = { PATH: process.env.PATH, HOME: cwd, TMPDIR: '/tmp', NETLIFY_INTERNAL_GIT: '1', NETLIFY_VERIFY_REPO_ROOT: checkout, GIT_AUTHOR_NAME: 'Independent Tester', GIT_AUTHOR_EMAIL: 'test@example.com', GIT_COMMITTER_NAME: 'Independent Tester', GIT_COMMITTER_EMAIL: 'test@example.com', } const git = (...args) => execFileSync('git', args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim() const names = ['café.txt', 'space name.txt', 'tab\tname.txt', 'line\nbreak.txt', 'quote"name.txt', 'back\\slash.txt', 'trailing\n'] const expected = { created: names.map(n => `created/${n}`), modified: names.map(n => `modified/${n}`), deleted: names.map(n => `deleted/${n}`) } try { git('init', '--quiet') git('config', 'core.quotePath', 'true') for (const category of Object.keys(expected)) mkdirSync(join(cwd, category)) for (const name of [...expected.modified, ...expected.deleted, 'before rename.txt']) writeFileSync(join(cwd, name), 'base content\n') git('add', '.') git('commit', '--quiet', '-m', 'base') const base = git('rev-parse', 'HEAD') for (const name of expected.created) writeFileSync(join(cwd, name), 'created content\n') for (const name of expected.modified) writeFileSync(join(cwd, name), 'modified content\n') for (const name of expected.deleted) rmSync(join(cwd, name)) renameSync(join(cwd, 'before rename.txt'), join(cwd, 'after rename.txt')) expected.created.push('after rename.txt') expected.deleted.push('before rename.txt') git('add', '.') git('commit', '--quiet', '-m', 'all change categories') const head = git('rev-parse', 'HEAD') mkdirSync(join(cwd, 'node_modules', '.bin'), { recursive: true }) writeFileSync(join(cwd, 'node_modules', '.bin', 'independent-netlify-plugin-local'), '#!/usr/bin/env node\nconsole.log("project-local")\n', { mode: 0o755 }) for (const [label, ref, files] of [['mixed changes plus rename', base, expected], ['empty diff', head, { created: [], modified: [], deleted: [] }]]) { const child = fork(join(checkout, 'packages/build/src/plugins/child/main.js'), [], { cwd, env: { ...env, CACHED_COMMIT_REF: ref }, execArgv: ['--no-warnings', '--loader', join(root, 'source-loader.mjs')], stdio: ['ignore', 'pipe', 'pipe', 'ipc'], }) let output = '' child.stdout.on('data', chunk => { output += chunk }) child.stderr.on('data', chunk => { output += chunk }) const timeout = setTimeout(() => child.kill(), 30000) const send = (eventName, payload) => callChild({ childProcess: child, eventName, payload, verbose: false }) try { await getEventFromChild(child, 'ready') const netlifyConfig = { build: { environment: {} } } const loaded = await send('load', { pluginPath: join(root, 'plugin.mjs'), inputs: { cwd, expected: files }, packageJson: {}, verbose: false, netlifyConfig }) assert.deepEqual(loaded.events, ['onBuild']) const result = await send('run', { event: 'onBuild', constants: { CACHE_DIR: join(cwd, 'cache') }, envChanges: {}, featureFlags: {}, netlifyConfig, otelCarrier: {} }) assert.equal(result.status.summary, 'All real plugin utility checks passed') assert.deepEqual(JSON.parse(result.status.text), files) assert.equal(output.trim(), 'project-local', 'Default stdout forwarding through plugin process') console.log(`PASS real child boot → IPC load → onBuild → getUtils → Git/execa: ${label}`) console.log(` ${Object.values(files).flat().length} exact file statuses and fileMatch results; local-bin defaults/override; omitted argv cwd/env; child exit errors; stdout forwarding`) } finally { clearTimeout(timeout) child.kill() await once(child, 'exit') if (output.trim() !== 'project-local') console.log(output) } } } finally { rmSync(cwd, { recursive: true, force: true }) }