diff --git a/packages/wxt/e2e/tests/zip.test.ts b/packages/wxt/e2e/tests/zip.test.ts index 3ff265aad..c370097bc 100644 --- a/packages/wxt/e2e/tests/zip.test.ts +++ b/packages/wxt/e2e/tests/zip.test.ts @@ -285,4 +285,26 @@ describe('Zipping', () => { expect(await project.fileExists(sourcesZip)).toBe(false); }, ); + + it('should include files in the zip when negated in zip.exclude', async () => { + const project = new TestProject({ + name: 'test', + version: '1.0.0', + }); + project.addFile( + 'entrypoints/background.ts', + 'export default defineBackground(() => {});', + ); + const unzipDir = project.resolvePath('.output/test-1.0.0-chrome'); + const sourcesZip = project.resolvePath('.output/test-1.0.0-chrome.zip'); + + await project.zip({ + zip: { + exclude: ['**/*.json', '!manifest.json'], + }, + }); + + await extract(sourcesZip, { dir: unzipDir }); + expect(await project.fileExists(unzipDir, 'manifest.json')).toBe(true); + }); }); diff --git a/packages/wxt/src/core/utils/__tests__/minimatch-multiple.test.ts b/packages/wxt/src/core/utils/__tests__/minimatch-multiple.test.ts new file mode 100644 index 000000000..34c078e13 --- /dev/null +++ b/packages/wxt/src/core/utils/__tests__/minimatch-multiple.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect } from 'vitest'; +import { minimatchMultiple } from '../minimatch-multiple'; + +describe('minimatchMultiple', () => { + it('should return false if the pattern array is undefined', () => { + const patterns = undefined; + const search = 'test.json'; + + expect(minimatchMultiple(search, patterns)).toBe(false); + }); + + it('should return false if the pattern array is empty', () => { + const patterns: string[] = []; + const search = 'test.json'; + + expect(minimatchMultiple(search, patterns)).toBe(false); + }); + + it('should return true if the pattern array contains a match', () => { + const patterns = ['test.yml', 'test.json']; + const search = 'test.json'; + + expect(minimatchMultiple(search, patterns)).toBe(true); + }); + + it('should return false if the pattern array does not contain a match', () => { + const patterns = ['test.yml', 'test.json']; + const search = 'test.txt'; + + expect(minimatchMultiple(search, patterns)).toBe(false); + }); + + it('should return false if the pattern matches a negative pattern', () => { + const patterns = ['test.*', '!test.json']; + const search = 'test.json'; + + expect(minimatchMultiple(search, patterns)).toBe(false); + }); + + it('should return false if the pattern matches a negative pattern, regardless of order', () => { + const patterns = ['!test.json', 'test.*']; + const search = 'test.json'; + + expect(minimatchMultiple(search, patterns)).toBe(false); + }); +}); diff --git a/packages/wxt/src/core/utils/minimatch-multiple.ts b/packages/wxt/src/core/utils/minimatch-multiple.ts new file mode 100644 index 000000000..be1bbf0e8 --- /dev/null +++ b/packages/wxt/src/core/utils/minimatch-multiple.ts @@ -0,0 +1,40 @@ +import { minimatch, MinimatchOptions } from 'minimatch'; + +/** + * Run [`minimatch`](https://npmjs.com/package/minimatch) against multiple + * patterns. + * + * Supports negated patterns, the order does not matter. If your `search` string + * matches any of the negative patterns, it will return `false`. + * + * @example + * ```ts + * minimatchMultiple('a.json', ['*.json', '!b.json']); // => true + * minimatchMultiple('b.json', ['*.json', '!b.json']); // => false + * ``` + */ +export function minimatchMultiple( + search: string, + patterns: string[] | undefined, + options?: MinimatchOptions, +): boolean { + if (patterns == null) return false; + + const negatePatterns: string[] = []; + const positivePatterns: string[] = []; + for (const pattern of patterns) { + if (pattern[0] === '!') negatePatterns.push(pattern.slice(1)); + else positivePatterns.push(pattern); + } + + if ( + negatePatterns.some((negatePattern) => + minimatch(search, negatePattern, options), + ) + ) + return false; + + return positivePatterns.some((positivePattern) => + minimatch(search, positivePattern, options), + ); +} diff --git a/packages/wxt/src/core/zip.ts b/packages/wxt/src/core/zip.ts index ceda07efd..70626d6ae 100644 --- a/packages/wxt/src/core/zip.ts +++ b/packages/wxt/src/core/zip.ts @@ -3,7 +3,6 @@ import path from 'node:path'; import fs from 'fs-extra'; import { safeFilename } from './utils/strings'; import { getPackageJson } from './utils/package'; -import { minimatch } from 'minimatch'; import { formatDuration } from './utils/time'; import { printFileList } from './utils/log/printFileList'; import { findEntrypoints, internalBuild } from './utils/building'; @@ -11,6 +10,7 @@ import { registerWxt, wxt } from './wxt'; import JSZip from 'jszip'; import glob from 'fast-glob'; import { normalizePath } from './utils/paths'; +import { minimatchMultiple } from './utils/minimatch-multiple'; /** * Build and zip the extension for distribution. @@ -122,8 +122,8 @@ async function zipDir( }) ).filter((relativePath) => { return ( - options?.include?.some((pattern) => minimatch(relativePath, pattern)) || - !options?.exclude?.some((pattern) => minimatch(relativePath, pattern)) + minimatchMultiple(relativePath, options?.include) || + !minimatchMultiple(relativePath, options?.exclude) ); }); const filesToZip = [