Skip to content
Merged
7 changes: 1 addition & 6 deletions src/generators/legacy-html/index.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

import { readFile, rm, writeFile, mkdir } from 'node:fs/promises';
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { join } from 'node:path';

import HTMLMinifier from '@minify-html/node';
Expand Down Expand Up @@ -176,11 +176,6 @@ export default {
// Define the output folder for API docs assets
const assetsFolder = join(output, 'assets');

// Removes the current assets directory to copy the new assets
// and prevent stale assets from existing in the output directory
// If the path does not exists, it will simply ignore and continue
await rm(assetsFolder, { recursive: true, force: true, maxRetries: 10 });

// Creates the assets folder if it does not exist
await mkdir(assetsFolder, { recursive: true });

Expand Down
125 changes: 125 additions & 0 deletions src/generators/legacy-html/utils/__tests__/safeCopy.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
'use strict';

import assert from 'node:assert';
import { mkdir, readFile, rm, utimes, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, it } from 'node:test';

import { safeCopy } from '../safeCopy.mjs';

describe('safeCopy', () => {
const testDir = join(import.meta.dirname, 'test-safe-copy');
const srcDir = join(testDir, 'src');
const targetDir = join(testDir, 'target');

beforeEach(async () => {
// Create test directories
await mkdir(srcDir, { recursive: true });
await mkdir(targetDir, { recursive: true });
});

afterEach(async () => {
// Clean up test directories
await rm(testDir, { recursive: true, force: true });
});

it('should copy new files that do not exist in target', async () => {
// Create a file in source
await writeFile(join(srcDir, 'file1.txt'), 'content1');

await safeCopy(srcDir, targetDir);

// Verify file was copied
const content = await readFile(join(targetDir, 'file1.txt'), 'utf-8');
assert.strictEqual(content, 'content1');
});

it('should copy multiple files', async () => {
// Create multiple files in source
await writeFile(join(srcDir, 'file1.txt'), 'content1');
await writeFile(join(srcDir, 'file2.txt'), 'content2');
await writeFile(join(srcDir, 'file3.txt'), 'content3');

await safeCopy(srcDir, targetDir);

// Verify all files were copied
const content1 = await readFile(join(targetDir, 'file1.txt'), 'utf-8');
const content2 = await readFile(join(targetDir, 'file2.txt'), 'utf-8');
const content3 = await readFile(join(targetDir, 'file3.txt'), 'utf-8');

assert.strictEqual(content1, 'content1');
assert.strictEqual(content2, 'content2');
assert.strictEqual(content3, 'content3');
});

it('should skip files with same size and older modification time', async () => {
// Create file in source with specific size
const content = 'same content';
await writeFile(join(srcDir, 'file1.txt'), content);

// Make source file old
const oldTime = new Date(Date.now() - 10000);
await utimes(join(srcDir, 'file1.txt'), oldTime, oldTime);

// Create target file with same size but different content and newer timestamp
await writeFile(join(targetDir, 'file1.txt'), 'other things');

await safeCopy(srcDir, targetDir);

// Verify file was not overwritten (source is older)
const targetContent = await readFile(join(targetDir, 'file1.txt'), 'utf-8');
assert.strictEqual(targetContent, 'other things');
});

it('should copy files when source has newer modification time', async () => {
// Create files in both directories
await writeFile(join(srcDir, 'file1.txt'), 'new content');
await writeFile(join(targetDir, 'file1.txt'), 'old content');

// Make target file older
const oldTime = new Date(Date.now() - 10000);
await utimes(join(targetDir, 'file1.txt'), oldTime, oldTime);

await safeCopy(srcDir, targetDir);

// Verify file was updated
const content = await readFile(join(targetDir, 'file1.txt'), 'utf-8');
assert.strictEqual(content, 'new content');
});

it('should copy files when sizes differ', async () => {
// Create files with different sizes
await writeFile(join(srcDir, 'file1.txt'), 'short');
await writeFile(join(targetDir, 'file1.txt'), 'much longer content');

await safeCopy(srcDir, targetDir);

// Verify file was updated
const content = await readFile(join(targetDir, 'file1.txt'), 'utf-8');
assert.strictEqual(content, 'short');
});

it('should handle empty source directory', async () => {
// Don't create any files in source
await safeCopy(srcDir, targetDir);

// Verify no error occurred - if we get here, the function succeeded
assert.ok(true);
Comment thread
ovflowd marked this conversation as resolved.
Outdated
});

it('should copy files with same size but different content when mtime is newer', async () => {
// Create files with same size but different content
await writeFile(join(srcDir, 'file1.txt'), 'abcde');
await writeFile(join(targetDir, 'file1.txt'), 'fghij');

// Make target older
const oldTime = new Date(Date.now() - 10000);
await utimes(join(targetDir, 'file1.txt'), oldTime, oldTime);

await safeCopy(srcDir, targetDir);

// Verify file was updated with source content
const content = await readFile(join(targetDir, 'file1.txt'), 'utf-8');
assert.strictEqual(content, 'abcde');
});
});
28 changes: 13 additions & 15 deletions src/generators/legacy-html/utils/safeCopy.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
'use strict';

import { readFile, writeFile, stat, readdir } from 'node:fs/promises';
import { copyFile, readdir, stat } from 'node:fs/promises';
import { join } from 'node:path';

/**
* Safely copies files from source to target directory, skipping files that haven't changed
* based on file stats (size and modification time)
* Copies files from source to target directory, skipping files that haven't changed.
* Uses synchronous stat checks for simplicity and copyFile for atomic operations.
*
* @param {string} srcDir - Source directory path
* @param {string} targetDir - Target directory path
Expand All @@ -17,22 +17,20 @@ export async function safeCopy(srcDir, targetDir) {
const sourcePath = join(srcDir, file);
const targetPath = join(targetDir, file);

const [sStat, tStat] = await Promise.allSettled([
stat(sourcePath),
stat(targetPath),
]);
const tStat = await stat(targetPath).catch(() => undefined);
Comment thread
ovflowd marked this conversation as resolved.
Outdated

const shouldWrite =
tStat.status === 'rejected' ||
sStat.value.size !== tStat.value.size ||
sStat.value.mtimeMs > tStat.value.mtimeMs;

if (!shouldWrite) {
// If target doesn't exist, copy immediately
if (!tStat) {
await copyFile(sourcePath, targetPath);
Comment thread
ovflowd marked this conversation as resolved.
Outdated
continue;
}

const fileContent = await readFile(sourcePath);
// Target exists, check if we need to update
const sStat = await stat(sourcePath);

await writeFile(targetPath, fileContent);
// Skip if target has same size and source is not newer
if (sStat.size !== tStat.size || sStat.mtimeMs > tStat.mtimeMs) {
await copyFile(sourcePath, targetPath);
Comment thread
ovflowd marked this conversation as resolved.
Outdated
}
Comment thread
ovflowd marked this conversation as resolved.
}
}