fix(zip)!: make includeSources/excludeSources follow standard allowlist behavior (#2114)
Co-authored-by: Eli <88557639+lishaduck@users.noreply.github.com> Co-authored-by: ТΞNSΛI <tensai@gmx.net> Co-authored-by: Aaron <aaronklinker1@gmail.com> Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com> Co-authored-by: Kai Gritun <kai@kaigritun.com>
This commit is contained in:
@@ -110,7 +110,7 @@ describe('Zipping', () => {
|
||||
expect(await project.pathExists(unzipDir, '.hidden-dir/file')).toBe(false);
|
||||
});
|
||||
|
||||
it('should not zip files inside hidden directories if only the directory is specified', async () => {
|
||||
it('should zip hidden files and directories when dotSources is enabled', async () => {
|
||||
const project = new TestProject({
|
||||
name: 'test',
|
||||
version: '1.0.0',
|
||||
@@ -127,17 +127,17 @@ describe('Zipping', () => {
|
||||
await project.zip({
|
||||
browser: 'firefox',
|
||||
zip: {
|
||||
includeSources: ['.hidden-dir'],
|
||||
dotSources: true,
|
||||
},
|
||||
});
|
||||
await extract(sourcesZip, { dir: unzipDir });
|
||||
expect(await project.pathExists(unzipDir, '.hidden-dir/file')).toBe(false);
|
||||
expect(await project.pathExists(unzipDir, '.hidden-dir/file')).toBe(true);
|
||||
expect(await project.pathExists(unzipDir, '.hidden-dir/nested/file')).toBe(
|
||||
false,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow zipping hidden files into sources when explicitly listed', async () => {
|
||||
it('should allow ignoring some hidden files', async () => {
|
||||
const project = new TestProject({
|
||||
name: 'test',
|
||||
version: '1.0.0',
|
||||
@@ -156,20 +156,57 @@ describe('Zipping', () => {
|
||||
await project.zip({
|
||||
browser: 'firefox',
|
||||
zip: {
|
||||
includeSources: ['.env', '.hidden-dir/file', '.hidden-dir/nested/**'],
|
||||
dotSources: true,
|
||||
excludeSources: ['.hidden-dir/nested'],
|
||||
},
|
||||
});
|
||||
await extract(sourcesZip, { dir: unzipDir });
|
||||
expect(await project.pathExists(unzipDir, '.env')).toBe(true);
|
||||
expect(await project.pathExists(unzipDir, '.hidden-dir/file')).toBe(true);
|
||||
expect(await project.pathExists(unzipDir, '.hidden-dir/nested/file1')).toBe(
|
||||
true,
|
||||
false,
|
||||
);
|
||||
expect(await project.pathExists(unzipDir, '.hidden-dir/nested/file2')).toBe(
|
||||
true,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not include all files when includeSources is provided', async () => {
|
||||
const project = new TestProject({
|
||||
name: 'test',
|
||||
version: '1.0.0',
|
||||
});
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
'export default defineBackground(() => {});',
|
||||
);
|
||||
project.addFile('utils/example.ts', 'export const x = 1;');
|
||||
project.addFile('secrets/api-key.txt', 'supersecret');
|
||||
project.addFile('cache/data.json', '{}');
|
||||
const unzipDir = project.resolvePath('.output/test-1.0.0-sources');
|
||||
const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip');
|
||||
|
||||
await project.zip({
|
||||
browser: 'firefox',
|
||||
zip: {
|
||||
includeSources: ['entrypoints/**', 'utils/**'],
|
||||
},
|
||||
});
|
||||
await extract(sourcesZip, { dir: unzipDir });
|
||||
|
||||
// Included files should be present
|
||||
expect(
|
||||
await project.pathExists(unzipDir, 'entrypoints/background.ts'),
|
||||
).toBe(true);
|
||||
expect(await project.pathExists(unzipDir, 'utils/example.ts')).toBe(true);
|
||||
|
||||
// Non-included files should NOT be present (allowlist behavior)
|
||||
expect(await project.pathExists(unzipDir, 'secrets/api-key.txt')).toBe(
|
||||
false,
|
||||
);
|
||||
expect(await project.pathExists(unzipDir, 'cache/data.json')).toBe(false);
|
||||
});
|
||||
|
||||
it('should exclude skipped entrypoints from respective browser sources zip', async () => {
|
||||
const project = new TestProject({
|
||||
name: 'test',
|
||||
@@ -188,8 +225,7 @@ describe('Zipping', () => {
|
||||
`export default defineContentScript({
|
||||
matches: ['*://*/*'],
|
||||
main(ctx) {},
|
||||
});
|
||||
`,
|
||||
});`,
|
||||
);
|
||||
const unzipDir = project.resolvePath('.output/test-1.0.0-sources');
|
||||
const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip');
|
||||
@@ -289,7 +325,8 @@ describe('Zipping', () => {
|
||||
|
||||
await project.zip({
|
||||
zip: {
|
||||
exclude: ['**/*.json', '!manifest.json'],
|
||||
// Exclude all JSON files except for ones named `manifest.json`
|
||||
exclude: ['**/!(manifest).json'],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -304,7 +304,7 @@ function resolveZipConfig(
|
||||
artifactTemplate:
|
||||
'{{name}}-{{packageVersion}}-{{browser}}{{modeSuffix}}.zip',
|
||||
sourcesRoot: root,
|
||||
includeSources: [],
|
||||
includeSources: mergedConfig.zip?.includeSources ?? ['**/*'],
|
||||
compressionLevel: 9,
|
||||
...mergedConfig.zip,
|
||||
zipSources:
|
||||
@@ -314,8 +314,6 @@ function resolveZipConfig(
|
||||
'**/node_modules',
|
||||
// WXT files
|
||||
'**/web-ext.config.ts',
|
||||
// Hidden files
|
||||
'**/.*',
|
||||
// Tests
|
||||
'**/__tests__/**',
|
||||
'**/*.+(test|spec).?(c|m)+(j|t)s?(x)',
|
||||
@@ -324,6 +322,7 @@ function resolveZipConfig(
|
||||
// From user
|
||||
...(mergedConfig.zip?.excludeSources ?? []),
|
||||
],
|
||||
dotSources: mergedConfig.zip?.dotSources ?? false,
|
||||
downloadPackages: mergedConfig.zip?.downloadPackages ?? [],
|
||||
downloadedPackagesDir,
|
||||
};
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { picomatchMultiple } from '../picomatch-multiple';
|
||||
|
||||
describe('picomatchMultiple', () => {
|
||||
it('should return false if the pattern array is undefined', () => {
|
||||
const patterns = undefined;
|
||||
const search = 'test.json';
|
||||
|
||||
expect(picomatchMultiple(search, patterns)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if the pattern array is empty', () => {
|
||||
const patterns: string[] = [];
|
||||
const search = 'test.json';
|
||||
|
||||
expect(picomatchMultiple(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(picomatchMultiple(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(picomatchMultiple(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(picomatchMultiple(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(picomatchMultiple(search, patterns)).toBe(false);
|
||||
});
|
||||
|
||||
it('should support extglob-like extension matching', () => {
|
||||
const patterns = ['content.[jt]s?(x)'];
|
||||
|
||||
expect(picomatchMultiple('content.ts', patterns)).toBe(true);
|
||||
expect(picomatchMultiple('content.jsx', patterns)).toBe(true);
|
||||
expect(picomatchMultiple('content.css', patterns)).toBe(false);
|
||||
});
|
||||
|
||||
it('should support nested paths', () => {
|
||||
const patterns = ['foo/**/*.ts'];
|
||||
|
||||
expect(picomatchMultiple('foo/bar/baz.ts', patterns)).toBe(true);
|
||||
expect(picomatchMultiple('foo/bar/baz.js', patterns)).toBe(false);
|
||||
});
|
||||
|
||||
it('should preserve include/exclude interaction used by zip filtering', () => {
|
||||
const include = ['special.txt'];
|
||||
const exclude = ['**/*.txt'];
|
||||
const search = 'special.txt';
|
||||
const shouldInclude =
|
||||
picomatchMultiple(search, include) || !picomatchMultiple(search, exclude);
|
||||
|
||||
expect(shouldInclude).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,38 +0,0 @@
|
||||
import picomatch, { PicomatchOptions } from 'picomatch';
|
||||
|
||||
/**
|
||||
* Run [`picomatch`](https://npmjs.com/package/picomatch) 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
|
||||
* picomatchMultiple('a.json', ['*.json', '!b.json']); // => true
|
||||
* picomatchMultiple('b.json', ['*.json', '!b.json']); // => false
|
||||
*/
|
||||
export function picomatchMultiple(
|
||||
search: string,
|
||||
patterns: string[] | undefined,
|
||||
options?: PicomatchOptions,
|
||||
): 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) =>
|
||||
picomatch(negatePattern, options)(search),
|
||||
)
|
||||
)
|
||||
return false;
|
||||
|
||||
return positivePatterns.some((positivePattern) =>
|
||||
picomatch(positivePattern, options)(search),
|
||||
);
|
||||
}
|
||||
@@ -285,6 +285,7 @@ export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
|
||||
artifactTemplate: '{{name}}-{{version}}.zip',
|
||||
includeSources: [],
|
||||
excludeSources: [],
|
||||
dotSources: false,
|
||||
exclude: [],
|
||||
sourcesRoot: fakeDir(),
|
||||
sourcesTemplate: '{{name}}-sources.zip',
|
||||
|
||||
@@ -11,7 +11,6 @@ import { registerWxt, wxt } from './wxt';
|
||||
import JSZip from 'jszip';
|
||||
import { glob } from 'tinyglobby';
|
||||
import { normalizePath } from './utils';
|
||||
import { picomatchMultiple } from './utils/picomatch-multiple';
|
||||
|
||||
/**
|
||||
* Build and zip the extension for distribution.
|
||||
@@ -70,7 +69,7 @@ export async function zip(config?: InlineConfig): Promise<string[]> {
|
||||
...skippedEntrypoints.map((entry) =>
|
||||
path.relative(wxt.config.zip.sourcesRoot, entry.inputPath),
|
||||
),
|
||||
].map((paths) => paths.replaceAll('\\', '/'));
|
||||
].map((paths) => paths.replaceAll('\\', '/')); // TODO: Use normalizePath?
|
||||
await wxt.hooks.callHook('zip:sources:start', wxt);
|
||||
const { overrides, files: downloadedPackages } =
|
||||
await downloadPrivatePackages();
|
||||
@@ -88,6 +87,7 @@ export async function zip(config?: InlineConfig): Promise<string[]> {
|
||||
}
|
||||
},
|
||||
additionalFiles: downloadedPackages,
|
||||
dot: wxt.config.zip.dotSources,
|
||||
});
|
||||
zipFiles.push(sourcesZipPath);
|
||||
await wxt.hooks.callHook('zip:sources:done', wxt, sourcesZipPath);
|
||||
@@ -118,22 +118,17 @@ async function zipDir(
|
||||
) => Promise<string | undefined | void> | string | undefined | void;
|
||||
additionalWork?: (archive: JSZip) => Promise<void> | void;
|
||||
additionalFiles?: string[];
|
||||
dot?: boolean;
|
||||
},
|
||||
): Promise<void> {
|
||||
const archive = new JSZip();
|
||||
const files = (
|
||||
await glob(['**/*', ...(options?.include || [])], {
|
||||
cwd: directory,
|
||||
// Ignore node_modules, otherwise this glob step takes forever
|
||||
ignore: ['**/node_modules'],
|
||||
onlyFiles: true,
|
||||
expandDirectories: false,
|
||||
})
|
||||
).filter((relativePath) => {
|
||||
return (
|
||||
picomatchMultiple(relativePath, options?.include) ||
|
||||
!picomatchMultiple(relativePath, options?.exclude)
|
||||
);
|
||||
// includeSources patterns are used directly (defaults to ['**/*'] from config)
|
||||
// excludeSources patterns are passed to glob's ignore option for efficient filtering
|
||||
const files = await glob(options?.include ?? ['**/*'], {
|
||||
cwd: directory,
|
||||
ignore: options?.exclude ?? [],
|
||||
onlyFiles: true,
|
||||
dot: options?.dot,
|
||||
});
|
||||
const filesToZip = [
|
||||
...files,
|
||||
|
||||
+48
-15
@@ -250,25 +250,39 @@ export interface InlineConfig {
|
||||
*/
|
||||
sourcesRoot?: string;
|
||||
/**
|
||||
* [Picomatch](https://www.npmjs.com/package/picomatch) patterns of files to
|
||||
* include when creating a ZIP of all your source code for Firefox. Patterns
|
||||
* are relative to your `config.zip.sourcesRoot`.
|
||||
* [Tinyglobby](https://npmjs.org/tinyglobby) patterns of files to include
|
||||
* when creating a ZIP of all your source code for Firefox. Patterns are
|
||||
* relative to your `config.zip.sourcesRoot`.
|
||||
*
|
||||
* This setting overrides `excludeSources`. So if a file matches both lists,
|
||||
* it is included in the ZIP.
|
||||
* Sources ZIP files are created using standard allowlist/blocklist
|
||||
* behavior:
|
||||
*
|
||||
* - You specify a pattern to "include" (via `includeSources`), then a pattern
|
||||
* to "exclude" from the included files (via `excludeSources`).
|
||||
*
|
||||
* By default, this option includes all files except for hidden files and
|
||||
* directories (files/directories starting with a `.`).
|
||||
*
|
||||
* If you want to include hidden files/directories in your sources ZIP, see
|
||||
* `InlineConfig.zip.dotSources`.
|
||||
*
|
||||
* @example
|
||||
* [
|
||||
* 'coverage', // Include the coverage directory in the `sourcesRoot`
|
||||
* ];
|
||||
* ['entrypoints/**', 'wxt.config.ts', 'package.json', 'tsconfig.json'];
|
||||
*/
|
||||
includeSources?: string[];
|
||||
/**
|
||||
* [Picomatch](https://www.npmjs.com/package/picomatch) patterns of files to
|
||||
* exclude when creating a ZIP of all your source code for Firefox. Patterns
|
||||
* are relative to your `config.zip.sourcesRoot`.
|
||||
* [Tinyglobby](https://npmjs.org/tinyglobby) patterns of files to exclude
|
||||
* when creating a ZIP of all your source code for Firefox. Patterns are
|
||||
* relative to your `config.zip.sourcesRoot`.
|
||||
*
|
||||
* Hidden files, node_modules, and tests are ignored by default.
|
||||
* By default, WXT excludes some files:
|
||||
*
|
||||
* - `node_modules`
|
||||
* - Tests files and directories
|
||||
* - Output directory
|
||||
*
|
||||
* Any values specified in this option will be merged with the ones above -
|
||||
* you cannot replace the default values, only add to them.
|
||||
*
|
||||
* @example
|
||||
* [
|
||||
@@ -277,13 +291,31 @@ export interface InlineConfig {
|
||||
*/
|
||||
excludeSources?: string[];
|
||||
/**
|
||||
* [Picomatch](https://www.npmjs.com/package/picomatch) patterns of files to
|
||||
* exclude when zipping the extension.
|
||||
* Include hidden files/directories in your sources ZIP.
|
||||
*
|
||||
* [Tinyglobby](https://npmjs.org/tinyglobby) does not match against files
|
||||
* and directory that start with a `.` by default. For example, if you need
|
||||
* to include a `.env` file, you need to set this to `true`, then exclude
|
||||
* other hidden files/directories in `excludeSources`.
|
||||
*
|
||||
* **Be very careful when this is enabled - WXT may include files with
|
||||
* secrets in your ZIP you did not intend to share with Mozilla or upload to
|
||||
* other places**. Make sure all hidden files you don't want to include are
|
||||
* added to `excludeSources`.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
dotSources?: boolean;
|
||||
/**
|
||||
* [Tinyglobby](https://npmjs.org/tinyglobby) patterns of files to exclude
|
||||
* when zipping the extension.
|
||||
*
|
||||
* @example
|
||||
* [
|
||||
* '**\/*.map', // Exclude all sourcemaps
|
||||
* ];
|
||||
*
|
||||
* @default [ ]
|
||||
*/
|
||||
exclude?: string[];
|
||||
/**
|
||||
@@ -1533,6 +1565,7 @@ export interface ResolvedConfig {
|
||||
sourcesTemplate: string;
|
||||
includeSources: string[];
|
||||
excludeSources: string[];
|
||||
dotSources: boolean;
|
||||
sourcesRoot: string;
|
||||
downloadedPackagesDir: string;
|
||||
downloadPackages: string[];
|
||||
@@ -1690,7 +1723,7 @@ export type WxtResolvedUnimportOptions = Partial<UnimportOptions> & {
|
||||
|
||||
/**
|
||||
* Package management utils built on top of
|
||||
* [`nypm`](https://www.npmjs.com/package/nypm)
|
||||
* [`nypm`](https://npmjs.org/package/nypm)
|
||||
*/
|
||||
export interface WxtPackageManager extends Nypm.PackageManager {
|
||||
addDependency: typeof Nypm.addDependency;
|
||||
|
||||
Reference in New Issue
Block a user