UNPKG

@factorialco/shadowdog

Version:

<img src="https://raw.githubusercontent.com/factorialco/shadowdog/refs/heads/main/logo.png" alt="drawing" width="100"/>

318 lines (317 loc) 14.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const vitest_1 = require("vitest"); const shadowdog_lock_1 = __importDefault(require("./shadowdog-lock")); const fs = __importStar(require("fs-extra")); const fs_1 = require("fs"); const events_1 = require("../events"); // Mock fs-extra - because apparently mocking file system operations is rocket science vitest_1.vi.mock('fs-extra', () => ({ ensureDir: vitest_1.vi.fn().mockResolvedValue(undefined), writeJSON: vitest_1.vi.fn().mockResolvedValue(undefined), readJSON: vitest_1.vi.fn().mockResolvedValue({}), pathExists: vitest_1.vi.fn().mockResolvedValue(false), })); // Mock fs writeFileSync vitest_1.vi.mock('fs', () => ({ writeFileSync: vitest_1.vi.fn(), readFileSync: vitest_1.vi.fn(), statSync: vitest_1.vi.fn(), })); // Mock glob vitest_1.vi.mock('glob', () => ({ sync: vitest_1.vi.fn().mockReturnValue([]), })); // Mock utils vitest_1.vi.mock('../utils', () => ({ logMessage: vitest_1.vi.fn(), readShadowdogVersion: vitest_1.vi.fn(() => '0.8.0'), computeCache: vitest_1.vi.fn((files, env, cmd) => `cache-${files.length}-${env.length}-${cmd.length}`), computeFileCacheName: vitest_1.vi.fn((cache, fileName) => `file-cache-${cache}-${fileName}`), processFiles: vitest_1.vi.fn((files) => files), // Mock processFiles to return files as-is })); (0, vitest_1.describe)('shadowdog-lock plugin', () => { let mockEventEmitter; let mockConfig; (0, vitest_1.beforeEach)(() => { mockEventEmitter = new events_1.ShadowdogEventEmitter(); mockConfig = { watchers: [ { files: ['src/test.ts'], environment: [], ignored: [], commands: [ { command: 'npm run test', artifacts: [{ output: 'test.json' }], }, ], }, ], defaultIgnoredFiles: [], }; vitest_1.vi.clearAllMocks(); }); (0, vitest_1.afterEach)(() => { vitest_1.vi.clearAllMocks(); }); (0, vitest_1.it)('should be a listener plugin', () => { (0, vitest_1.expect)(shadowdog_lock_1.default).toHaveProperty('listener'); (0, vitest_1.expect)(typeof shadowdog_lock_1.default.listener).toBe('function'); }); (0, vitest_1.it)('should not regenerate lock file after config is loaded', async () => { // Set up the listener shadowdog_lock_1.default.listener(mockEventEmitter, { path: '/tmp/shadowdog/lock' }); // Emit config loaded event mockEventEmitter.emit('configLoaded', { config: mockConfig }); // Wait for async operations await new Promise((resolve) => setTimeout(resolve, 0)); // Should not regenerate on config loaded anymore (0, vitest_1.expect)(fs.ensureDir).not.toHaveBeenCalled(); (0, vitest_1.expect)(fs.writeJSON).not.toHaveBeenCalled(); }); (0, vitest_1.it)('should regenerate lock file after task completion in daemon mode', async () => { // Set up the listener shadowdog_lock_1.default.listener(mockEventEmitter, { path: '/tmp/shadowdog/lock' }); // First emit config loaded event mockEventEmitter.emit('configLoaded', { config: mockConfig }); // Wait for config to be processed await new Promise((resolve) => setTimeout(resolve, 0)); // Clear previous calls vitest_1.vi.clearAllMocks(); // Simulate daemon mode by emitting generateStarted then allTasksComplete // to set isInGenerateMode to false mockEventEmitter.emit('generateStarted'); mockEventEmitter.emit('allTasksComplete'); // Wait for async operations await new Promise((resolve) => setTimeout(resolve, 0)); // Clear previous calls again vitest_1.vi.clearAllMocks(); // Now emit end event (should regenerate in daemon mode) mockEventEmitter.emit('end', { artifacts: [{ output: 'test.json' }] }); // Wait for async operations await new Promise((resolve) => setTimeout(resolve, 0)); (0, vitest_1.expect)(fs_1.writeFileSync).toHaveBeenCalled(); }); (0, vitest_1.it)('should not regenerate lock file on initialized event in watch mode', async () => { // Set up the listener shadowdog_lock_1.default.listener(mockEventEmitter, { path: '/tmp/shadowdog/lock' }); // First emit config loaded event mockEventEmitter.emit('configLoaded', { config: mockConfig }); // Wait for config to be processed await new Promise((resolve) => setTimeout(resolve, 0)); // Clear previous calls vitest_1.vi.clearAllMocks(); // Emit initialized event (should not regenerate in watch mode) mockEventEmitter.emit('initialized'); // Wait for async operations await new Promise((resolve) => setTimeout(resolve, 0)); // Should not regenerate on initialized event in watch mode (0, vitest_1.expect)(fs.ensureDir).not.toHaveBeenCalled(); (0, vitest_1.expect)(fs.writeJSON).not.toHaveBeenCalled(); }); (0, vitest_1.it)('should regenerate lock file after all tasks complete in generate mode', async () => { // Set up the listener shadowdog_lock_1.default.listener(mockEventEmitter, { path: '/tmp/shadowdog/lock' }); // First emit config loaded event mockEventEmitter.emit('configLoaded', { config: mockConfig }); // Wait for config to be processed await new Promise((resolve) => setTimeout(resolve, 0)); // Clear previous calls vitest_1.vi.clearAllMocks(); // Emit allTasksComplete event (generate mode) mockEventEmitter.emit('allTasksComplete'); // Wait for async operations await new Promise((resolve) => setTimeout(resolve, 0)); (0, vitest_1.expect)(fs_1.writeFileSync).toHaveBeenCalled(); }); (0, vitest_1.it)('should generate deterministic lock file based on config order', async () => { const multiWatcherConfig = { watchers: [ { files: ['src/a.ts'], environment: [], ignored: [], commands: [ { command: 'npm run build-a', artifacts: [{ output: 'a.json' }], }, ], }, { files: ['src/b.ts'], environment: [], ignored: [], commands: [ { command: 'npm run build-b', artifacts: [{ output: 'b.json' }], }, ], }, ], defaultIgnoredFiles: [], }; // Set up the listener shadowdog_lock_1.default.listener(mockEventEmitter, { path: '/tmp/shadowdog/lock' }); // Emit config loaded event mockEventEmitter.emit('configLoaded', { config: multiWatcherConfig }); // Emit allTasksComplete event (this is when lock file should be regenerated) mockEventEmitter.emit('allTasksComplete'); // Wait for async operations await new Promise((resolve) => setTimeout(resolve, 0)); (0, vitest_1.expect)(fs_1.writeFileSync).toHaveBeenCalledWith(vitest_1.expect.any(String), vitest_1.expect.stringContaining('"output": "a.json"'), 'utf8'); }); (0, vitest_1.it)('should handle errors gracefully', async () => { // Mock fs functions to throw error vitest_1.vi.mocked(fs_1.writeFileSync).mockImplementation(() => { throw new Error('Write error'); }); // Set up the listener shadowdog_lock_1.default.listener(mockEventEmitter, { path: '/tmp/shadowdog/lock' }); // Emit config loaded event mockEventEmitter.emit('configLoaded', { config: mockConfig }); mockEventEmitter.emit('allTasksComplete'); // Wait for async operations await new Promise((resolve) => setTimeout(resolve, 10)); // Should not throw - the plugin should handle errors gracefully // The function should complete without throwing (0, vitest_1.expect)(true).toBe(true); }); (0, vitest_1.it)('should handle concurrent calls with write promise protection', async () => { // Set up the listener shadowdog_lock_1.default.listener(mockEventEmitter, { path: '/tmp/shadowdog/lock' }); // Emit config loaded event mockEventEmitter.emit('configLoaded', { config: mockConfig }); // Emit multiple end events concurrently mockEventEmitter.emit('end', { artifacts: [{ output: 'test1.json' }] }); mockEventEmitter.emit('end', { artifacts: [{ output: 'test2.json' }] }); mockEventEmitter.emit('end', { artifacts: [{ output: 'test3.json' }] }); // Wait for all async operations await new Promise((resolve) => setTimeout(resolve, 50)); // The plugin should handle concurrent calls gracefully (0, vitest_1.expect)(fs_1.writeFileSync).toHaveBeenCalled(); }); (0, vitest_1.it)('should always write lock file regardless of tag filtering', async () => { const configWithTags = { watchers: [ { files: ['src/a.ts'], environment: [], ignored: [], commands: [ { command: 'npm run build-a', artifacts: [{ output: 'a.json' }], tags: ['production'], }, ], }, { files: ['src/b.ts'], environment: [], ignored: [], commands: [ { command: 'npm run build-b', artifacts: [{ output: 'b.json' }], tags: ['development'], }, ], }, ], defaultIgnoredFiles: [], }; // Set up the listener shadowdog_lock_1.default.listener(mockEventEmitter, { path: '/tmp/shadowdog/lock' }); // Emit config loaded event mockEventEmitter.emit('configLoaded', { config: configWithTags }); // Wait for async operations await new Promise((resolve) => setTimeout(resolve, 10)); // The lock file should be written (we can verify this works in integration tests) // For now, just verify the function doesn't throw (0, vitest_1.expect)(true).toBe(true); }); (0, vitest_1.it)('should include outputSha in lock file artifacts', async () => { const { readFileSync, statSync } = await Promise.resolve().then(() => __importStar(require('fs'))); const mockReadFileSync = readFileSync; const mockStatSync = statSync; // Mock file system responses for content SHA calculation mockStatSync.mockReturnValue({ isDirectory: () => false }); mockReadFileSync.mockReturnValue(Buffer.from('test file content')); const configWithArtifacts = { debounceTime: 100, watchers: [ { files: ['src/test.ts'], environment: ['NODE_ENV'], ignored: [], commands: [ { command: 'npm run build', artifacts: [{ output: 'dist/app.js' }, { output: 'dist/styles.css' }], }, ], }, ], plugins: [], defaultIgnoredFiles: [], }; // Set up the listener shadowdog_lock_1.default.listener(mockEventEmitter, { path: '/tmp/shadowdog/lock' }); // Emit config loaded event mockEventEmitter.emit('configLoaded', { config: configWithArtifacts }); // Wait for async operations await new Promise((resolve) => setTimeout(resolve, 10)); // Verify that writeFileSync was called with lock file containing outputSha (0, vitest_1.expect)(fs_1.writeFileSync).toHaveBeenCalled(); const mockWriteFileSync = fs_1.writeFileSync; const writeCall = mockWriteFileSync.mock.calls.find((call) => typeof call[0] === 'string' && call[0].includes('shadowdog-lock.json')); if (writeCall) { const lockFileContent = JSON.parse(writeCall[1]); (0, vitest_1.expect)(lockFileContent.artifacts).toBeDefined(); (0, vitest_1.expect)(lockFileContent.artifacts.length).toBeGreaterThan(0); (0, vitest_1.expect)(lockFileContent.artifacts[0]).toHaveProperty('outputSha'); (0, vitest_1.expect)(typeof lockFileContent.artifacts[0].outputSha).toBe('string'); (0, vitest_1.expect)(lockFileContent.artifacts[0].outputSha.length).toBeGreaterThan(0); } }); });