-
-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[astro-purgecss] Fix files modified without updating hash (#611)
* Fix purgecss not updating file names after purge * Add tests to astro purge css * Add pkg-pr-new * Fix workflow * docs(changeset): Fix files modified without updating hash
- Loading branch information
Showing
11 changed files
with
965 additions
and
75 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'astro-purgecss': minor | ||
--- | ||
|
||
Fix files modified without updating hash |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import { unlink, writeFile } from 'node:fs/promises'; | ||
import { basename, dirname } from 'node:path'; | ||
import { afterEach, describe, expect, it, vi } from 'vitest'; | ||
|
||
import { resolveOutputPath, writeCssFile } from './utils'; | ||
|
||
describe('resolveOutputPath', () => { | ||
it('should return the same path for non-windows platforms', () => { | ||
const originalPlatform = process.platform; | ||
Object.defineProperty(process, 'platform', { value: 'darwin' }); | ||
|
||
const inputPath = '/some/unix/path'; | ||
expect(resolveOutputPath(inputPath)).toBe(inputPath); | ||
|
||
Object.defineProperty(process, 'platform', { value: originalPlatform }); | ||
}); | ||
|
||
it('should remove trailing backslash and replace backslashes with forward slashes on windows', () => { | ||
const originalPlatform = process.platform; | ||
Object.defineProperty(process, 'platform', { value: 'win32' }); | ||
|
||
const inputPath = 'C:\\Users\\test\\path\\'; | ||
const expectedPath = 'C:/Users/test/path'; | ||
expect(resolveOutputPath(inputPath)).toBe(expectedPath); | ||
|
||
Object.defineProperty(process, 'platform', { value: originalPlatform }); | ||
}); | ||
|
||
it('should replace backslashes with forward slashes on windows without trailing backslash', () => { | ||
const originalPlatform = process.platform; | ||
Object.defineProperty(process, 'platform', { value: 'win32' }); | ||
|
||
const inputPath = 'C:\\Users\\test\\path'; | ||
const expectedPath = 'C:/Users/test/path'; | ||
expect(resolveOutputPath(inputPath)).toBe(expectedPath); | ||
|
||
Object.defineProperty(process, 'platform', { value: originalPlatform }); | ||
}); | ||
}); | ||
|
||
const mockCss = 'body { color: red; }'; | ||
const mockFile = '/path/to/astro.csjqp06s.css'; | ||
const mockOutDir = '/path/to/outdir'; | ||
const mockHash = 'abcdefg1'; | ||
|
||
vi.mock('node:fs/promises', () => ({ | ||
writeFile: vi.fn(() => Promise.resolve()), | ||
unlink: vi.fn(() => Promise.resolve()), | ||
readdir: vi.fn(() => Promise.resolve([])) | ||
})); | ||
|
||
vi.mock('node:path', () => ({ | ||
dirname: vi.fn(() => '/path/to'), | ||
basename: vi.fn((file) => file.split('/').pop() as string), | ||
join: vi.fn((...args) => args.join('/')) | ||
})); | ||
|
||
vi.mock('node:crypto', () => ({ | ||
createHash: vi.fn(() => ({ | ||
update: vi.fn().mockReturnThis(), | ||
digest: vi.fn().mockReturnValue(mockHash.repeat(5)) | ||
})) | ||
})); | ||
|
||
describe('writeCssFile', () => { | ||
afterEach(() => { | ||
vi.clearAllMocks(); | ||
}); | ||
|
||
it('should return undefined if no file is provided', async () => { | ||
const result = await writeCssFile({ css: mockCss, outDir: mockOutDir }); | ||
expect(result).toBeUndefined(); | ||
}); | ||
|
||
it('should write new file, delete old file, and update references', async () => { | ||
const newFile = '/path/to/astro.abcdefg1.css'; | ||
|
||
await writeCssFile({ css: mockCss, file: mockFile, outDir: mockOutDir }); | ||
|
||
expect(writeFile).toHaveBeenCalledWith(newFile, mockCss); | ||
expect(unlink).toHaveBeenCalledWith(mockFile); | ||
expect(dirname).toHaveBeenCalledWith(mockOutDir); | ||
expect(basename).toHaveBeenCalledWith(mockFile); | ||
expect(basename).toHaveBeenCalledWith(newFile); | ||
}); | ||
|
||
it('should return the new file name', async () => { | ||
const result = await writeCssFile({ | ||
css: mockCss, | ||
file: mockFile, | ||
outDir: mockOutDir | ||
}); | ||
expect(result).toBe('/path/to/astro.abcdefg1.css'); | ||
}); | ||
|
||
it('should handle errors gracefully', async () => { | ||
vi.mocked(writeFile).mockImplementation(() => | ||
Promise.reject(new Error('Write error')) | ||
); | ||
|
||
await expect( | ||
writeCssFile({ css: mockCss, file: mockFile, outDir: mockOutDir }) | ||
).rejects.toThrow('Write error'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import { createHash } from 'node:crypto'; | ||
import { readdir, readFile, unlink, writeFile } from 'node:fs/promises'; | ||
import { basename, dirname, join } from 'node:path'; | ||
|
||
async function replaceStringInFile( | ||
filePath: string, | ||
searchValue: string, | ||
replaceValue: string | ||
) { | ||
try { | ||
const fileContent = await readFile(filePath, 'utf8'); | ||
if (fileContent.includes(searchValue)) { | ||
const re = new RegExp(searchValue, 'g'); | ||
const newContent = fileContent.replace(re, replaceValue); | ||
await writeFile(filePath, newContent, 'utf8'); | ||
} | ||
} catch (err) { | ||
console.error(`Error processing file ${filePath}: ${err}`); | ||
} | ||
} | ||
|
||
async function replaceStringInDirectory( | ||
directory: string, | ||
searchValue: string, | ||
replaceValue: string | ||
) { | ||
try { | ||
const files = await readdir(directory, { withFileTypes: true }); | ||
for (const file of files) { | ||
const fullPath = join(directory, file.name); | ||
if (file.isDirectory()) { | ||
await replaceStringInDirectory(fullPath, searchValue, replaceValue); | ||
} else if (file.isFile()) { | ||
await replaceStringInFile(fullPath, searchValue, replaceValue); | ||
} | ||
} | ||
} catch (err) { | ||
console.error(`Error processing directory ${directory}: ${err}`); | ||
} | ||
} | ||
|
||
export function resolveOutputPath(outputPath: string): string { | ||
if (process.platform !== 'win32') return outputPath; | ||
|
||
// Remove trailing backslash if present | ||
outputPath = outputPath.replace(/\\+$/, ''); | ||
|
||
// Replace all backslashes with forward slashes | ||
return outputPath.replace(/\\/g, '/'); | ||
} | ||
|
||
export async function writeCssFile({ | ||
css, | ||
file, | ||
outDir | ||
}: { | ||
css: string; | ||
file?: string; | ||
outDir: string; | ||
}) { | ||
if (!file) return; | ||
|
||
// Get content hash before writing to file | ||
const hash = createHash('sha256').update(css).digest('hex').substring(0, 8); | ||
|
||
// Generate new file name with hash | ||
// Astro orignal hash is 8 characters long | ||
const newFile = `${file.slice(0, -13)}.${hash}.css`; | ||
|
||
// Write purged CSS to new file | ||
await writeFile(newFile, css); | ||
|
||
// Remove old file | ||
await unlink(file); | ||
|
||
// Replace old name references with new file name | ||
await replaceStringInDirectory( | ||
dirname(outDir), // Search from parent directory | ||
basename(file), | ||
basename(newFile) | ||
); | ||
|
||
return newFile; | ||
} |
Oops, something went wrong.